Skip to content

Commit

Permalink
Most examples moved from Scala 2 -> Scala 3 (#3404)
Browse files Browse the repository at this point in the history
Co-authored-by: Pawel Stawicki <[email protected]>
  • Loading branch information
amorfis and Pawel Stawicki authored Dec 20, 2023
1 parent 62c3fda commit 0aff30b
Show file tree
Hide file tree
Showing 74 changed files with 551 additions and 894 deletions.
51 changes: 40 additions & 11 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ val scala2Versions = List(scala2_12, scala2_13)
val scala2And3Versions = scala2Versions ++ List(scala3)
val scala2_13And3Versions = List(scala2_13, scala3)
val codegenScalaVersions = List(scala2_12)
val examplesScalaVersions = List(scala2_13)
val examplesScalaVersions = List(scala3)
val documentationScalaVersion = scala2_13

lazy val clientTestServerPort = settingKey[Int]("Port to run the client interpreter test server on")
Expand Down Expand Up @@ -238,8 +238,8 @@ lazy val rawAllAggregates = core.projectRefs ++
play29Client.projectRefs ++
tests.projectRefs ++
perfTests.projectRefs ++
examples2.projectRefs ++
examples.projectRefs ++
examples3.projectRefs ++
documentation.projectRefs ++
openapiCodegenCore.projectRefs ++
openapiCodegenSbt.projectRefs ++
Expand Down Expand Up @@ -542,7 +542,7 @@ lazy val perfTests: ProjectMatrix = (projectMatrix in file("perf-tests"))
.settings(http4sTapir := { (genPerfTestTask("http4s.Tapir", "OneRoute")).value })
.settings(http4sVanillaMulti := { (genPerfTestTask("http4s.VanillaMulti", "MultiRoute")).value })
.settings(http4sTapirMulti := { (genPerfTestTask("http4s.TapirMulti", "MultiRoute")).value })
.jvmPlatform(scalaVersions = examplesScalaVersions)
.jvmPlatform(scalaVersions = List(scala2_13))
.dependsOn(core, akkaHttpServer, http4sServer)

// integrations
Expand Down Expand Up @@ -2015,10 +2015,10 @@ lazy val openapiCodegenCli: ProjectMatrix = (projectMatrix in file("openapi-code

// other

lazy val examples: ProjectMatrix = (projectMatrix in file("examples"))
lazy val examples2: ProjectMatrix = (projectMatrix in file("examples2"))
.settings(commonJvmSettings)
.settings(
name := "tapir-examples",
name := "tapir-examples2",
libraryDependencies ++= Seq(
"dev.zio" %% "zio-interop-cats" % Versions.zioInteropCats,
"org.typelevel" %% "cats-effect" % Versions.catsEffect,
Expand All @@ -2043,7 +2043,7 @@ lazy val examples: ProjectMatrix = (projectMatrix in file("examples"))
publishArtifact := false,
Compile / run / fork := true
)
.jvmPlatform(scalaVersions = examplesScalaVersions)
.jvmPlatform(scalaVersions = List(scala2_13))
.dependsOn(
akkaHttpServer,
pekkoHttpServer,
Expand Down Expand Up @@ -2077,25 +2077,54 @@ lazy val examples: ProjectMatrix = (projectMatrix in file("examples"))
protobuf
)

lazy val examples3: ProjectMatrix = (projectMatrix in file("examples3"))
lazy val examples: ProjectMatrix = (projectMatrix in file("examples"))
.settings(commonJvmSettings)
.settings(
name := "tapir-examples3",
name := "tapir-examples",
libraryDependencies ++= Seq(
"com.softwaremill.sttp.apispec" %% "asyncapi-circe-yaml" % Versions.sttpApispec,
"com.softwaremill.sttp.client3" %% "core" % Versions.sttp,
"com.softwaremill.sttp.client3" %% "pekko-http-backend" % Versions.sttp,
"com.softwaremill.sttp.client3" %% "async-http-client-backend-fs2" % Versions.sttp,
"com.softwaremill.sttp.client3" %% "async-http-client-backend-zio" % Versions.sttp,
"com.softwaremill.sttp.client3" %% "async-http-client-backend-cats" % Versions.sttp,
"com.github.jwt-scala" %% "jwt-circe" % Versions.jwtScala,
"org.http4s" %% "http4s-dsl" % Versions.http4s,
"org.http4s" %% "http4s-circe" % Versions.http4s,
"org.http4s" %% "http4s-blaze-server" % Versions.http4sBlazeServer,
"com.softwaremill.sttp.client3" %% "core" % Versions.sttp
"io.opentelemetry" % "opentelemetry-sdk" % Versions.openTelemetry,
"io.opentelemetry" % "opentelemetry-sdk-metrics" % Versions.openTelemetry,
"io.opentelemetry" % "opentelemetry-exporter-otlp" % Versions.openTelemetry,
scalaTest.value
),
libraryDependencies ++= loggerDependencies,
publishArtifact := false
)
.jvmPlatform(scalaVersions = List(scala3))
.jvmPlatform(scalaVersions = examplesScalaVersions)
.dependsOn(
datadogMetrics,
prometheusMetrics,
opentelemetryMetrics,
zioMetrics,
circeJson,
http4sServer,
pekkoHttpServer,
armeriaServer,
nettyServer,
jdkhttpServer,
nettyServerCats,
http4sClient,
picklerJson,
sttpClient,
swaggerUiBundle
swaggerUiBundle,
http4sServerZio,
nettyServerZio,
zioHttpServer,
zioJson,
redocBundle,
sttpStubServer,
asyncapiDocs,
iron
)

//TODO this should be invoked by compilation process, see #https://github.com/scalameta/mdoc/issues/355
Expand Down
74 changes: 74 additions & 0 deletions doc/endpoint/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,80 @@ The iron codecs contain a validator which apply the constraint to validated valu

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`

### Validation

When using `iron` in the server e.g. in case classes that JSON request body
is parsed to, some additional steps need to be taken to properly
report `iron` validation errors.

[Iron](https://github.com/Iltotore/iron) is operating on type level while regular tapir
validation works on case classes created from parsed JSON. When `iron` types are used
in a case class, and passed values are invalid for `iron` types, creation is impossible because `iron`
does not allow creating guarded type instance.
Because it is not possible to create case class for `ServerInterpreter` it looks like JSON parsing error not
like validation error. In such case no error message is displayed to user.

To properly report `iron` errors it is necessary to recognize them in failure intereptor.
Custom JSON parsing is necessary anyway so custom exception can be thrown in case of `iron`
refinement error and then matched in failure interceptor.

Example for `circe`:

```scala
case class IronException(error: String) extends Exception(error)

inline given (using inline constraint: Constraint[Int, Positive]): Decoder[Age] = summon[Decoder[Int]].map(unrefinedValue =>
unrefinedValue.refineEither[Positive] match
case Right(value) => value
case Left(errorMessage) => throw IronException(s"Could not refine value $unrefinedValue: $errorMessage")
)
```

Then failure handler matching `IronException` is needed. Remember to create the interceptor:

```scala
private def failureDetailMessage(failure: DecodeResult.Failure): Option[String] = failure match {
case Error(_, JsonDecodeException(_, IronException(errorMessage))) => Some(errorMessage)
case Error(_, IronException(errorMessage)) => Some(errorMessage)
case other => FailureMessages.failureDetailMessage(other)
}

private def failureMessage(ctx: DecodeFailureContext): String = {
val base = FailureMessages.failureSourceMessage(ctx.failingInput)
val detail = failureDetailMessage(ctx.failure)
FailureMessages.combineSourceAndDetail(base, detail)
}

def ironFailureHandler[T[_]] = new DefaultDecodeFailureHandler[T](
DefaultDecodeFailureHandler.respond,
failureMessage,
DefaultDecodeFailureHandler.failureResponse
)

def ironDecodeFailureInterceptor[T[_]] = new DecodeFailureInterceptor[T](ironFailureHandler[T])
```

...and add it to server options:

```scala
override def run = NettyCatsServer
.io()
.use { server =>
// Don't forget to add the interceptor to server options
val optionsWithInterceptor = server.options.prependInterceptor(ironDecodeFailureInterceptor)
for {
binding <- server
.port(port)
.host(host)
.options(optionsWithInterceptor)
.addEndpoint(endpoint)
.start()
//...
}
}
```


## Enumeratum integration

The `tapir-enumeratum` module provides schemas, validators and codecs for [Enumeratum](https://github.com/lloydmeta/enumeratum)
Expand Down
2 changes: 1 addition & 1 deletion doc/examples.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Examples

The [examples](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples) and [examples3](https://github.com/softwaremill/tapir/tree/master/examples3/src/main/scala/sttp/tapir/examples3) sub-projects (the latter containing Scala 3-only code) contains a number of runnable tapir usage examples, using various interpreters and showcasing different features.
The [examples](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples) and [examples2](https://github.com/softwaremill/tapir/tree/master/examples2/src/main/scala/sttp/tapir/examples2) sub-projects (the latter containing Scala 2-only code) contains a number of runnable tapir usage examples, using various interpreters and showcasing different features.

## Generate a tapir project

Expand Down
26 changes: 13 additions & 13 deletions examples/src/main/scala/sttp/tapir/examples/BooksExample.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package sttp.tapir.examples

import com.typesafe.scalalogging.StrictLogging
import sttp.tapir.generic.auto._
import sttp.tapir.generic.auto.*

object BooksExample extends App with StrictLogging {
type Limit = Option[Int]
Expand All @@ -16,9 +16,9 @@ object BooksExample extends App with StrictLogging {
/** Descriptions of endpoints used in the example.
*/
object Endpoints {
import io.circe.generic.auto._
import sttp.tapir._
import sttp.tapir.json.circe._
import io.circe.generic.auto.*
import sttp.tapir.*
import sttp.tapir.json.circe.*

// All endpoints report errors as strings, and have the common path prefix '/books'
private val baseEndpoint = endpoint.errorOut(stringBody).in("books")
Expand Down Expand Up @@ -83,7 +83,7 @@ object BooksExample extends App with StrictLogging {

//

import Endpoints._
import Endpoints.*
import sttp.tapir.server.ServerEndpoint
import scala.concurrent.Future

Expand Down Expand Up @@ -112,7 +112,7 @@ object BooksExample extends App with StrictLogging {
Right[String, Vector[Book]](Library.getBooks(query))
}

// interpreting the endpoint description and converting it to an akka-http route, providing the logic which
// interpreting the endpoint description and converting it to an pekko-http route, providing the logic which
// should be run when the endpoint is invoked.
List(
addBook.serverLogic((bookAddLogic _).tupled),
Expand All @@ -125,29 +125,29 @@ object BooksExample extends App with StrictLogging {
import sttp.tapir.swagger.bundle.SwaggerInterpreter

// interpreting the endpoint descriptions as yaml openapi documentation
// exposing the docs using SwaggerUI endpoints, interpreted as an akka-http route
// exposing the docs using SwaggerUI endpoints, interpreted as an pekko-http route
SwaggerInterpreter().fromEndpoints(List(addBook, booksListing, booksListingByGenre), "The Tapir Library", "1.0")
}

def startServer(serverEndpoints: List[ServerEndpoint[Any, Future]]): Unit = {
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import org.apache.pekko.actor.ActorSystem
import org.apache.pekko.http.scaladsl.Http

import scala.concurrent.Await
import scala.concurrent.duration._
import scala.concurrent.duration.*

import sttp.tapir.server.akkahttp.AkkaHttpServerInterpreter
import sttp.tapir.server.pekkohttp.PekkoHttpServerInterpreter

implicit val actorSystem: ActorSystem = ActorSystem()
import actorSystem.dispatcher
val routes = AkkaHttpServerInterpreter().toRoute(serverEndpoints)
val routes = PekkoHttpServerInterpreter().toRoute(serverEndpoints)
Await.result(Http().newServerAt("localhost", 8080).bindFlow(routes), 1.minute)

logger.info("Server started")
}

def makeClientRequest(): Unit = {
import sttp.client3._
import sttp.client3.*
import sttp.tapir.client.sttp.SttpClientInterpreter

val client = SttpClientInterpreter().toQuickClient(booksListing, Some(uri"http://localhost:8080"))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package sttp.tapir.examples3
package sttp.tapir.examples

import com.typesafe.scalalogging.StrictLogging
import sttp.tapir.server.netty.{NettyFutureServer, NettyFutureServerBinding}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package sttp.tapir.examples3
package sttp.tapir.examples

import cats.effect.*
import cats.syntax.all.*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package sttp.tapir.examples

import com.linecorp.armeria.server.Server
import scala.concurrent.Future
import sttp.client3.{HttpURLConnectionBackend, Identity, SttpBackend, UriContext, asStringAlways, basicRequest}
import sttp.capabilities.armeria.ArmeriaStreams
import sttp.client3.{HttpURLConnectionBackend, Identity, SttpBackend, UriContext, asStringAlways, basicRequest}
import sttp.tapir.server.armeria.{ArmeriaFutureServerInterpreter, TapirService}
import sttp.tapir.{PublicEndpoint, endpoint, query, stringBody}
import sttp.tapir.*

import scala.concurrent.Future

object HelloWorldArmeriaServer extends App {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package sttp.tapir.examples

import cats.effect._
import cats.syntax.all._
import cats.effect.*
import cats.syntax.all.*
import org.http4s.HttpRoutes
import org.http4s.server.Router
import org.http4s.blaze.server.BlazeServerBuilder
import sttp.client3._
import sttp.tapir._
import org.http4s.server.Router
import sttp.client3.*
import sttp.tapir.*
import sttp.tapir.server.http4s.Http4sServerInterpreter

import scala.concurrent.ExecutionContext
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package sttp.tapir.examples

import sttp.client3.{HttpURLConnectionBackend, Identity, Response, SttpBackend, UriContext, asStringAlways, basicRequest}
import sttp.model.StatusCode
import sttp.tapir.server.jdkhttp._
import sttp.tapir.{PublicEndpoint, endpoint, query, stringBody}
import sttp.tapir.server.jdkhttp.*
import sttp.tapir.*

object HelloWorldJdkHttpServer extends App {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
package sttp.tapir.examples

import cats.effect.IO
import cats.effect.IOApp
import cats.effect.{IO, IOApp}
import sttp.client3.{HttpURLConnectionBackend, Identity, SttpBackend, UriContext, asStringAlways, basicRequest}
import sttp.model.StatusCode
import sttp.tapir.{PublicEndpoint, endpoint, query, stringBody}
import sttp.tapir.server.netty.cats.NettyCatsServer
import sttp.tapir.*

object HelloWorldNettyCatsServer extends IOApp.Simple {
// One endpoint on GET /hello with query parameter `name`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package sttp.tapir.examples
import sttp.client3.{HttpURLConnectionBackend, Identity, SttpBackend, UriContext, asStringAlways, basicRequest}
import sttp.model.StatusCode
import sttp.tapir.server.netty.{NettyFutureServer, NettyFutureServerBinding}
import sttp.tapir.{PublicEndpoint, endpoint, query, stringBody}
import sttp.tapir.*

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.Duration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ package sttp.tapir.examples
import org.apache.pekko.actor.ActorSystem
import org.apache.pekko.http.scaladsl.Http
import org.apache.pekko.http.scaladsl.server.Route
import sttp.client3._
import sttp.tapir._
import sttp.client3.*
import sttp.tapir.*
import sttp.tapir.server.pekkohttp.PekkoHttpServerInterpreter

import scala.concurrent.duration.*
import scala.concurrent.{Await, Future}
import scala.concurrent.duration._

object HelloWorldPekkoServer extends App {
implicit val actorSystem: ActorSystem = ActorSystem()
Expand All @@ -24,14 +24,16 @@ object HelloWorldPekkoServer extends App {
PekkoHttpServerInterpreter().toRoute(helloWorld.serverLogicSuccess(name => Future.successful(s"Hello, $name!")))

// starting the server
val bindAndCheck = Http().newServerAt("localhost", 8080).bindFlow(helloWorldRoute).map { _ =>
val bindAndCheck = Http().newServerAt("localhost", 8080).bindFlow(helloWorldRoute).map { binding =>
// testing
val backend: SttpBackend[Identity, Any] = HttpURLConnectionBackend()
val result: String = basicRequest.response(asStringAlways).get(uri"http://localhost:8080/hello?name=Frodo").send(backend).body
println("Got result: " + result)

assert(result == "Hello, Frodo!")

binding
}

Await.result(bindAndCheck.transformWith { r => actorSystem.terminate().transform(_ => r) }, 1.minute)
Await.result(bindAndCheck.flatMap(_.terminate(1.minute)), 1.minute)
}
Loading

0 comments on commit 0aff30b

Please sign in to comment.