From 993c4e7c928f7fb09fc420d7e6ff6bd30328ccee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Stawicki?= Date: Wed, 15 Jan 2025 10:21:56 +0100 Subject: [PATCH] Example: respond with 404 if None returned from server logic (#4257) --- .../errors/optionalValueExample.scala | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 examples/src/main/scala/sttp/tapir/examples/errors/optionalValueExample.scala diff --git a/examples/src/main/scala/sttp/tapir/examples/errors/optionalValueExample.scala b/examples/src/main/scala/sttp/tapir/examples/errors/optionalValueExample.scala new file mode 100644 index 0000000000..82455c4f18 --- /dev/null +++ b/examples/src/main/scala/sttp/tapir/examples/errors/optionalValueExample.scala @@ -0,0 +1,65 @@ +// {cat=Error handling; effects=Future; server=Pekko HTTP; JSON=circe}: Optional returned from the server logic, resulting in 404 if None + +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.12 +//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.11.12 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.12 +//> using dep com.softwaremill.sttp.client3::core:3.10.2 + +package sttp.tapir.examples.errors + +import io.circe.generic.auto.* +import io.circe.parser.parse +import org.apache.pekko.actor.ActorSystem +import org.apache.pekko.http.scaladsl.Http +import sttp.client3.{HttpURLConnectionBackend, Identity, SttpBackend, UriContext, basicRequest} +import sttp.model.StatusCode +import sttp.tapir.* +import sttp.tapir.generic.auto.* +import sttp.tapir.json.circe.* +import sttp.tapir.server.pekkohttp.PekkoHttpServerInterpreter + +import scala.concurrent.{Await, Future} +import scala.concurrent.duration.* + +@main def optionalValueExample(): Unit = + + case class Beer(name: String, volumeInLiters: Double) + + val bartenderEndpoint = endpoint + .get + .in("beer" / query[Int]("age")) + // Optional value from serverLogic, responding with 404 "Not Found" when logic returns None + .out(oneOf( + oneOfVariantExactMatcher(StatusCode.NotFound, jsonBody[Option[Beer]])(None), + oneOfVariantValueMatcher(StatusCode.Ok, jsonBody[Option[Beer]]) { + case Some(_) => true + } + )) + + // + + val bartenderServerEndpoint = bartenderEndpoint.serverLogic { + case a if a < 18 => Future.successful(Right(None)) + case _ => Future.successful(Right(Some(Beer("IPA", 0.5)))) + } + + + given actorSystem: ActorSystem = ActorSystem() + import actorSystem.dispatcher + val routes = PekkoHttpServerInterpreter().toRoute(bartenderServerEndpoint) + + val serverBinding = Http().newServerAt("localhost", 8080).bindFlow(routes).map { binding => + val backend: SttpBackend[Identity, Any] = HttpURLConnectionBackend() + + val response1 = basicRequest.get(uri"http://localhost:8080/beer?age=15").send(backend) + assert(response1.code == StatusCode.NotFound) + + val response2 = basicRequest.get(uri"http://localhost:8080/beer?age=21").send(backend) + println("Got result: " + response2.body) + val beerEither = response2.body.flatMap(parse).flatMap(_.as[Beer]) + assert(beerEither == Right(Beer("IPA", 0.5))) + + binding + } + + val _ = Await.result(serverBinding, 1.minute)