diff --git a/README.md b/README.md index a180f201d5..dc4aea81c9 100644 --- a/README.md +++ b/README.md @@ -4,20 +4,10 @@ [![CI](https://github.com/softwaremill/tapir/workflows/CI/badge.svg)](https://github.com/softwaremill/tapir/actions?query=workflow%3A%22CI%22) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.softwaremill.sttp.tapir/tapir-core_2.13/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.softwaremill.sttp.tapir/tapir-core_2.13) -## Why tapir? - -* **type-safety**: compile-time guarantees, develop-time completions, read-time information -* **declarative**: separate the shape of the endpoint (the "what"), from the server logic (the "how") -* **OpenAPI / Swagger integration**: generate documentation from endpoint descriptions -* **observability**: leverage the metadata to report rich metrics and tracing information -* **abstraction**: re-use common endpoint definitions, as well as individual inputs/outputs -* **library, not a framework**: integrates with your stack - ## Intro -With tapir, you can describe HTTP API endpoints as immutable Scala values. Each endpoint can contain a number of -input parameters, error-output parameters, and normal-output parameters. An endpoint specification can be -interpreted as: +With tapir, you can describe HTTP API endpoints as immutable Scala values. Each endpoint can contain a number of +input and output parameters. An endpoint specification can be interpreted as: * a server, given the "business logic": a function, which computes output parameters based on input parameters. Currently supported: @@ -39,14 +29,24 @@ interpreted as: * [OpenAPI](https://tapir.softwaremill.com/en/latest/docs/openapi.html) * [AsyncAPI](https://tapir.softwaremill.com/en/latest/docs/asyncapi.html) +Depending on how you prefer to explore the library, take a look at one of the [examples](https://tapir.softwaremill.com/en/latest/examples.html) +or [head over to the docs](https://tapir.softwaremill.com/en/latest/index.html) for a more detailed description of how tapir works! + +## Why tapir? + +* **type-safety**: compile-time guarantees, develop-time completions, read-time information +* **declarative**: separate the shape of the endpoint (the "what"), from the server logic (the "how") +* **OpenAPI / Swagger integration**: generate documentation from endpoint descriptions +* **observability**: leverage the metadata to report rich metrics and tracing information +* **abstraction**: re-use common endpoint definitions, as well as individual inputs/outputs +* **library, not a framework**: integrates with your stack + ## Adopters Is your company already using tapir? We're continually expanding the "adopters" section in the documentation; the more the merrier! It would be great to feature your company's logo, but in order to do that, we'll need written permission to avoid any legal misunderstandings. Please email us at [tapir@softwaremill.com](mailto:tapir@softwaremill.com) from your company's email with a link to your logo (if we can use it, of course!) or with details who to kindly ask for permission to feature the logo in tapir's documentation. We'll handle the rest. -We're seeing tapir's download numbers going steadily up; as we're nearing 1.0, the additional confidence boost for newcomers will help us to build tapir's ecosystem and make it thrive. Thank you! :) - |||| | :---: | :---: | :---: | | Adobe | Colisweb | Swissborg | @@ -64,16 +64,16 @@ import io.circe.generic.auto._ type Limit = Int type AuthToken = String -case class BooksFromYear(genre: String, year: Int) +case class BooksQuery(genre: String, year: Int) case class Book(title: String) // Define an endpoint -val booksListing: PublicEndpoint[(BooksFromYear, Limit, AuthToken), String, List[Book], Any] = +val booksListing: PublicEndpoint[(BooksQuery, Limit, AuthToken), String, List[Book], Any] = endpoint .get - .in(("books" / path[String]("genre") / path[Int]("year")).mapTo[BooksFromYear]) + .in(("books" / path[String]("genre") / path[Int]("year")).mapTo[BooksQuery]) .in(query[Limit]("limit").description("Maximum number of books to retrieve")) .in(header[AuthToken]("X-Auth-Token")) .errorOut(stringBody) @@ -96,7 +96,7 @@ import akka.http.scaladsl.server.Route import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits.global -def bookListingLogic(bfy: BooksFromYear, +def bookListingLogic(bfy: BooksQuery, limit: Limit, at: AuthToken): Future[Either[String, List[Book]]] = Future.successful(Right(List(Book("The Sorrows of Young Werther")))) @@ -113,7 +113,7 @@ import sttp.client3._ val booksListingRequest: Request[DecodeResult[Either[String, List[Book]]], Any] = SttpClientInterpreter() .toRequest(booksListing, Some(uri"http://localhost:8080")) - .apply((BooksFromYear("SF", 2016), 20, "xyz-abc-123")) + .apply((BooksQuery("SF", 2016), 20, "xyz-abc-123")) ``` ## Documentation @@ -125,15 +125,7 @@ tapir documentation is available at [tapir.softwaremill.com](http://tapir.softwa Add the following dependency: ```sbt -"com.softwaremill.sttp.tapir" %% "tapir-core" % "1.0.0-RC2" -``` - -Partial unification is now enabled by default from Scala 2.13. However, if you're using Scala 2.12 or older, then -you'll need partial unification enabled in the compiler (alternatively, you'll need to manually provide type -arguments in some cases): - -```sbt -scalacOptions += "-Ypartial-unification" +"com.softwaremill.sttp.tapir" %% "tapir-core" % "1.0.0-RC3" ``` Then, import: @@ -144,7 +136,15 @@ import sttp.tapir._ And finally, type `endpoint.` and see where auto-complete gets you! ---- +### Scala 2.12 + +Partial unification is now enabled by default from Scala 2.13. However, if you're using Scala 2.12 or older, then +you'll need partial unification enabled in the compiler (alternatively, you'll need to manually provide type +arguments in some cases): + +```sbt +scalacOptions += "-Ypartial-unification" +``` Sidenote for scala 2.12.4 and higher: if you encounter an issue with compiling your project because of a `StackOverflowException` related to [this](https://github.com/scala/bug/issues/10604) scala bug, @@ -166,7 +166,7 @@ sttp is a family of Scala HTTP-related projects, and currently includes: ## Contributing -Tapir is an early stage project. Everything might change. All suggestions welcome :) +All suggestions welcome :) See the list of [issues](https://github.com/softwaremill/tapir/issues) and pick one! Or report your own. diff --git a/build.sbt b/build.sbt index 90f6073a5a..bc7da8e9d2 100644 --- a/build.sbt +++ b/build.sbt @@ -324,7 +324,7 @@ lazy val core: ProjectMatrix = (projectMatrix in file("core")) libraryDependencies ++= { CrossVersion.partialVersion(scalaVersion.value) match { case Some((3, _)) => - Seq("com.softwaremill.magnolia1_3" %%% "magnolia" % "1.1.2") + Seq("com.softwaremill.magnolia1_3" %%% "magnolia" % "1.1.4") case _ => Seq( "com.softwaremill.magnolia1_2" %%% "magnolia" % "1.1.2", @@ -511,9 +511,9 @@ lazy val refined: ProjectMatrix = (projectMatrix in file("integrations/refined") "io.circe" %%% "circe-refined" % Versions.circe % Test ) ) - .jvmPlatform(scalaVersions = scala2Versions) + .jvmPlatform(scalaVersions = scala2And3Versions) .jsPlatform( - scalaVersions = scala2Versions, + scalaVersions = scala2And3Versions, settings = commonJsSettings ++ Seq( libraryDependencies ++= Seq( "io.github.cquiroz" %%% "scala-java-time" % Versions.jsScalaJavaTime % Test @@ -899,7 +899,7 @@ lazy val serverTests: ProjectMatrix = (projectMatrix in file("server/tests")) .settings( name := "tapir-server-tests", libraryDependencies ++= Seq( - "com.softwaremill.sttp.client3" %% "httpclient-backend-fs2" % Versions.sttp + "com.softwaremill.sttp.client3" %% "fs2" % Versions.sttp ) ) .dependsOn(tests, sttpStubServer) @@ -1152,7 +1152,7 @@ lazy val zio1HttpServer: ProjectMatrix = (projectMatrix in file("server/zio1-htt .settings(commonJvmSettings) .settings( name := "tapir-zio1-http-server", - libraryDependencies ++= Seq("dev.zio" %% "zio-interop-cats" % Versions.zio1InteropCats % Test, "io.d11" %% "zhttp" % "1.0.0.0-RC27") + libraryDependencies ++= Seq("dev.zio" %% "zio-interop-cats" % Versions.zio1InteropCats % Test, "io.d11" %% "zhttp" % "1.0.0.0-RC29") ) .jvmPlatform(scalaVersions = scala2And3Versions) .dependsOn(serverCore, zio1, serverTests % Test) @@ -1161,7 +1161,7 @@ lazy val zioHttpServer: ProjectMatrix = (projectMatrix in file("server/zio-http- .settings(commonJvmSettings) .settings( name := "tapir-zio-http-server", - libraryDependencies ++= Seq("dev.zio" %% "zio-interop-cats" % Versions.zioInteropCats % Test, "io.d11" %% "zhttp" % "2.0.0-RC7") + libraryDependencies ++= Seq("dev.zio" %% "zio-interop-cats" % Versions.zioInteropCats % Test, "io.d11" %% "zhttp" % "2.0.0-RC9") ) .jvmPlatform(scalaVersions = scala2And3Versions) .dependsOn(serverCore, zio, serverTests % Test) @@ -1174,7 +1174,7 @@ lazy val awsLambda: ProjectMatrix = (projectMatrix in file("serverless/aws/lambd name := "tapir-aws-lambda", libraryDependencies ++= loggerDependencies, libraryDependencies ++= Seq( - "com.softwaremill.sttp.client3" %% "httpclient-backend-fs2" % Versions.sttp + "com.softwaremill.sttp.client3" %% "fs2" % Versions.sttp ) ) .jvmPlatform(scalaVersions = scala2And3Versions) @@ -1351,8 +1351,8 @@ lazy val sttpClient: ProjectMatrix = (projectMatrix in file("client/sttp-client" scalaVersions = scala2And3Versions, settings = commonJvmSettings ++ Seq( libraryDependencies ++= Seq( - "com.softwaremill.sttp.client3" %% "httpclient-backend-fs2" % Versions.sttp % Test, - "com.softwaremill.sttp.client3" %% "httpclient-backend-zio" % Versions.sttp % Test, + "com.softwaremill.sttp.client3" %% "fs2" % Versions.sttp % Test, + "com.softwaremill.sttp.client3" %% "zio" % Versions.sttp % Test, "com.softwaremill.sttp.shared" %% "fs2" % Versions.sttpShared % Optional, "com.softwaremill.sttp.shared" %% "zio" % Versions.sttpShared % Optional ), @@ -1388,7 +1388,7 @@ lazy val sttpClientWsZio1: ProjectMatrix = (projectMatrix in file("client/sttp-c scalaVersions = scala2And3Versions, settings = commonJvmSettings ++ Seq( libraryDependencies ++= Seq( - "com.softwaremill.sttp.client3" %% "httpclient-backend-zio1" % Versions.sttp % Test, + "com.softwaremill.sttp.client3" %% "zio1" % Versions.sttp % Test, "com.softwaremill.sttp.shared" %% "zio1" % Versions.sttpShared ) ) @@ -1515,7 +1515,8 @@ lazy val examples: ProjectMatrix = (projectMatrix in file("examples")) zioJson, vertxServer, vertxServerCats, - vertxServerZio + vertxServerZio, + finatraServer ) lazy val examples3: ProjectMatrix = (projectMatrix in file("examples3")) diff --git a/client/sttp-client/src/main/scalajvm/sttp/tapir/client/sttp/ws/zio/WebSocketToZioPipe.scala b/client/sttp-client/src/main/scalajvm/sttp/tapir/client/sttp/ws/zio/WebSocketToZioPipe.scala index b8d48e75b1..4f289ee18b 100644 --- a/client/sttp-client/src/main/scalajvm/sttp/tapir/client/sttp/ws/zio/WebSocketToZioPipe.scala +++ b/client/sttp-client/src/main/scalajvm/sttp/tapir/client/sttp/ws/zio/WebSocketToZioPipe.scala @@ -6,8 +6,8 @@ import sttp.tapir.client.sttp.WebSocketToPipe import sttp.tapir.model.WebSocketFrameDecodeFailure import sttp.tapir.{DecodeResult, WebSocketBodyOutput} import sttp.ws.{WebSocket, WebSocketFrame} -import zio.Task -import zio.stream.Stream +import zio.{Task, ZIO} +import zio.stream.{Stream, ZStream} import scala.reflect.ClassTag @@ -24,13 +24,13 @@ class WebSocketToZioPipe[R <: ZioStreams with WebSockets] extends WebSocketToPip def decode(frame: WebSocketFrame): F[Either[Unit, Option[RESP]]] = o.responses.decode(frame) match { case failure: DecodeResult.Failure => - Task.fail(new WebSocketFrameDecodeFailure(frame, failure)) + ZIO.fail(new WebSocketFrameDecodeFailure(frame, failure)) case DecodeResult.Value(v) => - Task.right[Option[RESP]](Some(v)) + ZIO.right[Option[RESP]](Some(v)) } def raiseBadAccumulator[T](acc: WebSocketFrame, current: WebSocketFrame): F[T] = - Task.fail( + ZIO.fail( new WebSocketFrameDecodeFailure( current, DecodeResult.Error( @@ -54,20 +54,20 @@ class WebSocketToZioPipe[R <: ZioStreams with WebSockets] extends WebSocketToPip }).map(None -> _) else (acc match { - case None => Task.some(frame) - case Some(x: A) => Task.some(f(x, frame)) + case None => ZIO.some(frame) + case Some(x: A) => ZIO.some(f(x, frame)) case Some(bad) => raiseBadAccumulator(bad, frame) }).map(acc => acc -> Left(())) - val receives = Stream + val receives = ZStream .repeatZIO(ws.receive()) .mapAccumZIO[Any, Throwable, Option[WebSocketFrame], Either[Unit, Option[RESP]]]( None ) { // left - ignore; right - close or response case (acc, _: WebSocketFrame.Close) if !o.decodeCloseResponses => - Task.succeed(acc -> Right(None)) + ZIO.succeed(acc -> Right(None)) case (acc, _: WebSocketFrame.Pong) if o.ignorePong => - Task.succeed(acc -> Left(())) + ZIO.succeed(acc -> Left(())) case (acc, WebSocketFrame.Ping(p)) if o.autoPongOnPing => ws.send(WebSocketFrame.Pong(p)).as(acc -> Left(())) case (prev, frame @ WebSocketFrame.Text(_, last, _)) => @@ -75,7 +75,7 @@ class WebSocketToZioPipe[R <: ZioStreams with WebSockets] extends WebSocketToPip case (prev, frame @ WebSocketFrame.Binary(_, last, _)) => concatOrDecode(prev, frame, last)((l, r) => r.copy(payload = l.payload ++ r.payload)) case (_, frame) => - Task.fail( + ZIO.fail( new WebSocketFrameDecodeFailure( frame, DecodeResult.Error( diff --git a/client/sttp-client/src/test/scalajvm/sttp/tapir/client/sttp/SttpClientStreamingZioTests.scala b/client/sttp-client/src/test/scalajvm/sttp/tapir/client/sttp/SttpClientStreamingZioTests.scala index 91739c0306..af188d03b0 100644 --- a/client/sttp-client/src/test/scalajvm/sttp/tapir/client/sttp/SttpClientStreamingZioTests.scala +++ b/client/sttp-client/src/test/scalajvm/sttp/tapir/client/sttp/SttpClientStreamingZioTests.scala @@ -3,13 +3,13 @@ package sttp.tapir.client.sttp import sttp.capabilities.zio.ZioStreams import sttp.tapir.client.tests.ClientStreamingTests import zio.Chunk -import zio.stream.{Stream, ZPipeline} +import zio.stream.{Stream, ZPipeline, ZStream} class SttpClientStreamingZioTests extends SttpClientZioTests[ZioStreams] with ClientStreamingTests[ZioStreams] { override def wsToPipe: WebSocketToPipe[ZioStreams] = implicitly override val streams: ZioStreams = ZioStreams - override def mkStream(s: String): Stream[Throwable, Byte] = Stream.fromChunk(Chunk.fromArray(s.getBytes("utf-8"))) + override def mkStream(s: String): Stream[Throwable, Byte] = ZStream.fromChunk(Chunk.fromArray(s.getBytes("utf-8"))) override def rmStream(s: Stream[Throwable, Byte]): String = { zio.Runtime.default.unsafeRun( s.via(ZPipeline.utf8Decode).runCollect.map(_.fold("")(_ ++ _)) diff --git a/client/sttp-client/src/test/scalajvm/sttp/tapir/client/sttp/SttpClientWebSocketZioTests.scala b/client/sttp-client/src/test/scalajvm/sttp/tapir/client/sttp/SttpClientWebSocketZioTests.scala index b28408be34..197f82e316 100644 --- a/client/sttp-client/src/test/scalajvm/sttp/tapir/client/sttp/SttpClientWebSocketZioTests.scala +++ b/client/sttp-client/src/test/scalajvm/sttp/tapir/client/sttp/SttpClientWebSocketZioTests.scala @@ -5,7 +5,7 @@ import sttp.capabilities.WebSockets import sttp.capabilities.zio.ZioStreams import sttp.tapir.client.sttp.ws.zio._ import sttp.tapir.client.tests.ClientWebSocketTests -import zio.stream.Stream +import zio.stream.{Stream, ZStream} class SttpClientWebSocketZioTests extends SttpClientZioTests[WebSockets with ZioStreams] with ClientWebSocketTests[ZioStreams] { override val streams: ZioStreams = ZioStreams @@ -19,7 +19,7 @@ class SttpClientWebSocketZioTests extends SttpClientZioTests[WebSockets with Zio IO.delay { zio.Runtime.default .unsafeRunToFuture( - Stream(as: _*).viaFunction(p).take(receiveCount).runCollect.map(_.toList) + ZStream(as: _*).viaFunction(p).take(receiveCount).runCollect.map(_.toList) ) .future } diff --git a/core/src/main/scala-2/sttp/tapir/macros/SchemaMacros.scala b/core/src/main/scala-2/sttp/tapir/macros/SchemaMacros.scala index 90af348ae9..c126c6d28a 100644 --- a/core/src/main/scala-2/sttp/tapir/macros/SchemaMacros.scala +++ b/core/src/main/scala-2/sttp/tapir/macros/SchemaMacros.scala @@ -69,7 +69,7 @@ class CreateDerivedEnumerationSchema[T](validator: Validator.Enumeration[T]) { ): Schema[T] = { val v = encode.fold(validator)(e => validator.encode(e)) - val s0 = Schema.string.validate(v) + val s0 = Schema(schemaType).validate(v) default.fold(s0)(d => s0.default(d, encode.map(e => e(d)))) } } diff --git a/core/src/main/scala/sttp/tapir/Schema.scala b/core/src/main/scala/sttp/tapir/Schema.scala index ab4c339a61..631ad91dab 100644 --- a/core/src/main/scala/sttp/tapir/Schema.scala +++ b/core/src/main/scala/sttp/tapir/Schema.scala @@ -318,19 +318,19 @@ object Schema extends LowPrioritySchema with SchemaCompanionMacros { /** Annotations which are used during automatic schema derivation, or semi-automatic schema derivation using [[Schema.derived]]. */ object annotations { - class description(val text: String) extends StaticAnnotation - class encodedExample(val example: Any) extends StaticAnnotation - class default[T](val default: T, val encoded: Option[Any] = None) extends StaticAnnotation - class format(val format: String) extends StaticAnnotation - class deprecated extends StaticAnnotation - class hidden extends StaticAnnotation - class encodedName(val name: String) extends StaticAnnotation + class description(val text: String) extends StaticAnnotation with Serializable + class encodedExample(val example: Any) extends StaticAnnotation with Serializable + class default[T](val default: T, val encoded: Option[Any] = None) extends StaticAnnotation with Serializable + class format(val format: String) extends StaticAnnotation with Serializable + class deprecated extends StaticAnnotation with Serializable + class hidden extends StaticAnnotation with Serializable + class encodedName(val name: String) extends StaticAnnotation with Serializable /** Adds the `v` validator to the schema using [[Schema.validate]]. Note that the type of the validator must match exactly the type of * the class/field. This is not checked at compile-time, and might cause run-time exceptions. To validate elements of collections or * [[Option]]s, use [[validateEach]]. */ - class validate[T](val v: Validator[T]) extends StaticAnnotation + class validate[T](val v: Validator[T]) extends StaticAnnotation with Serializable /** Adds the `v` validators to elements of the schema, when the annotated class or field is a collection or [[Option]]. The type of the * validator must match exactly the type of the collection's elements. This is not checked at compile-time, and might cause run-time @@ -342,8 +342,8 @@ object Schema extends LowPrioritySchema with SchemaCompanionMacros { * ) * }}} */ - class validateEach[T](val v: Validator[T]) extends StaticAnnotation - class customise(val f: Schema[_] => Schema[_]) extends StaticAnnotation + class validateEach[T](val v: Validator[T]) extends StaticAnnotation with Serializable + class customise(val f: Schema[_] => Schema[_]) extends StaticAnnotation with Serializable } } diff --git a/core/src/main/scala/sttp/tapir/SchemaType.scala b/core/src/main/scala/sttp/tapir/SchemaType.scala index bd7753bcee..06b395ce9e 100644 --- a/core/src/main/scala/sttp/tapir/SchemaType.scala +++ b/core/src/main/scala/sttp/tapir/SchemaType.scala @@ -60,7 +60,7 @@ object SchemaType { override def as[TT]: SchemaType[TT] = SDateTime() } - trait SProductField[T] { + trait SProductField[T] extends Serializable { type FieldType def name: FieldName def schema: Schema[FieldType] diff --git a/core/src/main/scala/sttp/tapir/Validator.scala b/core/src/main/scala/sttp/tapir/Validator.scala index ec10a2b6e3..cdfc2fda28 100644 --- a/core/src/main/scala/sttp/tapir/Validator.scala +++ b/core/src/main/scala/sttp/tapir/Validator.scala @@ -90,8 +90,12 @@ object Validator extends ValidatorMacros { sealed trait Primitive[T] extends Validator[T] { def doValidate(t: T): ValidationResult override def apply(t: T): List[ValidationError[T]] = doValidate(t) match { - case ValidationResult.Valid => Nil - case ValidationResult.Invalid(customMessage) => List(ValidationError(this, t, Nil, customMessage)) + case ValidationResult.Valid => Nil + case ValidationResult.Invalid(customMessages) => + customMessages match { + case Nil => List(ValidationError(this, t, Nil, None)) + case l => l.map(m => ValidationError(this, t, Nil, Some(m))) + } } } case class Min[T](value: T, exclusive: Boolean)(implicit val valueIsNumeric: Numeric[T]) extends Primitive[T] { @@ -199,9 +203,10 @@ object Validator extends ValidatorMacros { sealed trait ValidationResult object ValidationResult { case object Valid extends ValidationResult - case class Invalid(customMessage: Option[String] = None) extends ValidationResult + case class Invalid(customMessage: List[String] = Nil) extends ValidationResult object Invalid { - def apply(customMessage: String): Invalid = Invalid(Some(customMessage)) + def apply(customMessage: String): Invalid = Invalid(List(customMessage)) + def apply(customMessage: String, customMessages: String*): Invalid = Invalid(customMessage :: customMessages.toList) } def validWhen(condition: Boolean): ValidationResult = if (condition) Valid else Invalid() diff --git a/core/src/main/scala/sttp/tapir/internal/package.scala b/core/src/main/scala/sttp/tapir/internal/package.scala index 1b517f82d2..130233ff7e 100644 --- a/core/src/main/scala/sttp/tapir/internal/package.scala +++ b/core/src/main/scala/sttp/tapir/internal/package.scala @@ -3,7 +3,7 @@ package sttp.tapir import sttp.model.{ContentTypeRange, MediaType, Method} import sttp.monad.MonadError import sttp.tapir.EndpointOutput.WebSocketBodyWrapper -import sttp.tapir.typelevel.{BinaryTupleOp, ParamConcat} +import sttp.tapir.typelevel.BinaryTupleOp import java.nio.charset.{Charset, StandardCharsets} import scala.collection.immutable diff --git a/core/src/test/scalajvm/sttp/tapir/SchemaSerialisationTest.scala b/core/src/test/scalajvm/sttp/tapir/SchemaSerialisationTest.scala new file mode 100644 index 0000000000..09f7274d7d --- /dev/null +++ b/core/src/test/scalajvm/sttp/tapir/SchemaSerialisationTest.scala @@ -0,0 +1,82 @@ +package sttp.tapir + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers +import sttp.tapir.Schema.SName +import sttp.tapir.Schema.annotations.{validate, validateEach} +import sttp.tapir.generic.auto._ + +import java.io._ + +class SchemaSerialisationTest extends AnyFlatSpec with Matchers { + + case class Person(@validate(Validator.minLength(3): Validator[String]) name: String, @validateEach(Validator.min(18)) age: Option[Int]) + case class Family(person1: Person, person2: Person, others: List[Person]) + + sealed trait Entity { + def kind: String + } + case class User(firstName: String, lastName: String) extends Entity { + def kind: String = "user" + } + case class Organization(name: String) extends Entity { + def kind: String = "org" + } + + private val schemasToSerialise = List( + Schema.string, + Schema.string + .description("x") + .encodedExample("y") + .validate(Validator.minLength(1)) + .deprecated(true) + .format("z") + .name(SName("a", List("b"))) + .default("y", Some("Y")), + Schema.schemaForInstant, + Schema.schemaForUUID, + Schema.string.asOption, + Schema.string.asIterable[List], + implicitly[Schema[Person]], + implicitly[Schema[Family]], + Schema + .oneOfUsingField[Entity, String](_.kind, _.toString)("user" -> implicitly[Schema[User]], "org" -> implicitly[Schema[Organization]]) + ) + + for (schema <- schemasToSerialise) { + it should s"serialise and deserialize $schema" in { + val output = new ByteArrayOutputStream() + val objectOutput = new ObjectOutputStream(output) + objectOutput.writeObject(schema) + objectOutput.close() + + val input = new ObjectInputStreamWithCustomClassLoader(new ByteArrayInputStream(output.toByteArray)) + val deserialized = input.readObject.asInstanceOf[Schema[Any]] + deserialized shouldBe schema + } + } + + it should "run validation on a deserialized object" in { + val schema = Schema.derived[Person] + val output = new ByteArrayOutputStream() + val objectOutput = new ObjectOutputStream(output) + objectOutput.writeObject(schema) + objectOutput.close() + + val input = new ObjectInputStreamWithCustomClassLoader(new ByteArrayInputStream(output.toByteArray)) + val deserialized = input.readObject.asInstanceOf[Schema[Any]] + deserialized shouldBe schema + + val p = Person("x", Some(10)) + schema.applyValidation(p) shouldBe deserialized.applyValidation(p) + } + + // needed so that tests pass also when run from sbt + // see https://stackoverflow.com/questions/60750717/spark-java-lang-classcastexception-cannot-assign-instance-of-scala-collection + class ObjectInputStreamWithCustomClassLoader(input: InputStream) extends ObjectInputStream(input) { + override def resolveClass(desc: java.io.ObjectStreamClass): Class[_] = { + try { Class.forName(desc.getName, false, getClass.getClassLoader) } + catch { case _: ClassNotFoundException => super.resolveClass(desc) } + } + } +} diff --git a/doc/conf.py b/doc/conf.py index d10a79e679..56ae499234 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -61,9 +61,9 @@ # built documents. # # The short X.Y version. -version = u'0.x' +version = u'1.x' # The full version, including alpha/beta/rc tags. -release = u'0.x' +release = u'1.x' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/doc/docs/asyncapi.md b/doc/docs/asyncapi.md index 0d7dad6295..f76bc7b6be 100644 --- a/doc/docs/asyncapi.md +++ b/doc/docs/asyncapi.md @@ -4,7 +4,7 @@ To use, add the following dependencies: ```scala "com.softwaremill.sttp.tapir" %% "tapir-asyncapi-docs" % "@VERSION@" -"com.softwaremill.sttp.tapir" %% "tapir-asyncapi-circe-yaml" % "@VERSION@" +"com.softwaremill.sttp.apispec" %% "asyncapi-circe-yaml" % "..." // see https://github.com/softwaremill/sttp-apispec ``` Tapir contains a case class-based model of the asyncapi data structures in the `asyncapi/asyncapi-model` subproject (the diff --git a/doc/docs/openapi.md b/doc/docs/openapi.md index 37a451fd20..0de2f0be46 100644 --- a/doc/docs/openapi.md +++ b/doc/docs/openapi.md @@ -1,5 +1,11 @@ # Generating OpenAPI documentation +To expose documentation, endpoints first need to be interpreted into an OpenAPI yaml or json. Then, the generated +description of our API can be exposed using a UI such as Swagger or Redoc. + +These two operations can be done in a single step, using the `SwaggerInterpreter` or `RedocInterpreter`. Or, if needed, +these steps can be done separately, giving you complete control over the process. + ## Generating and exposing documentation in a single step ### Using Swagger @@ -7,12 +13,13 @@ To generate OpenAPI documentation and expose it using the Swagger UI in a single step, first add the dependency: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-bundle" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-bundle" % "1.0.0-RC3" ``` -Then, you can interpret a list of endpoints, as server endpoints exposing the Swagger UI, using `SwaggerInterpreter`. -The result - a list of file-serving endpoints - will be configured to use the yaml corresponding to the passed -endpoints. The swagger endpoints will need in turn to be interpreted using your server interpreter. For example: +Then, you can interpret a list of endpoints using `SwaggerInterpreter`. The result will be a list of file-serving +server endpoints, which use the yaml corresponding to the endpoints passed originally. These swagger endpoints, together +with the endpoints for which the documentation is generated, will need in turn to be interpreted using your server +interpreter. For example: ```scala mdoc:compile-only import sttp.tapir._ @@ -48,12 +55,12 @@ for details. Similarly as above, you'll need the following dependency: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-redoc-bundle" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-redoc-bundle" % "1.0.0-RC3" ``` And the server endpoints can be generated using the `sttp.tapir.redoc.bundle.RedocInterpreter` class. -## Generating OpenAPI documentation +## Generating OpenAPI documentation separately To generate the docs in the OpenAPI yaml format, add the following dependencies: @@ -62,8 +69,8 @@ To generate the docs in the OpenAPI yaml format, add the following dependencies: "com.softwaremill.sttp.apispec" %% "openapi-circe-yaml" % "..." // see https://github.com/softwaremill/sttp-apispec ``` -Tapir contains a case class-based model of the openapi data structures in the `openapi/openapi-model` subproject (the -model is independent from all other tapir modules and can be used stand-alone). +The case-class based model of the openapi data structures is present in the [sttp-apispec](https://github.com/softwaremill/sttp-apispec) +project. An endpoint can be converted to an instance of the model by importing the `sttp.tapir.docs.openapi.OpenAPIDocsInterpreter` object: @@ -123,6 +130,43 @@ import sttp.apispec.openapi.circe._ println(Printer.spaces2.print(docs.asJson)) ``` +## Exposing generated OpenAPI documentation + +Exposing the OpenAPI can be done using [Swagger UI](https://swagger.io/tools/swagger-ui/) or +[Redoc](https://github.com/Redocly/redoc). You can either both interpret endpoints to OpenAPI's yaml and expose +them in a single step (see above), or you can do that separately. + +The modules `tapir-swagger-ui` and `tapir-redoc` contain server endpoint definitions, which given the documentation in +yaml format, will expose it using the given context path. To use, add as a dependency either +`tapir-swagger-ui`: +```scala +"com.softwaremill.sttp.tapir" %% "tapir-swagger-ui" % "@VERSION@" +``` + +or `tapir-redoc`: +```scala +"com.softwaremill.sttp.tapir" %% "tapir-redoc" % "@VERSION@" +``` + +Then, you'll need to pass the server endpoints to your server interpreter. For example, using akka-http: + +```scala mdoc:compile-only +import sttp.apispec.openapi.circe.yaml._ +import sttp.tapir._ +import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter +import sttp.tapir.server.akkahttp.AkkaHttpServerInterpreter +import sttp.tapir.swagger.SwaggerUI + +import scala.concurrent.Future +import scala.concurrent.ExecutionContext.Implicits.global + +val myEndpoints: Seq[AnyEndpoint] = ??? +val docsAsYaml: String = OpenAPIDocsInterpreter().toOpenAPI(myEndpoints, "My App", "1.0").toYaml + +// add to your akka routes +val swaggerUIRoute = AkkaHttpServerInterpreter().toRoute(SwaggerUI[Future](docsAsYaml)) +``` + ## Options Options can be customised by providing an instance of `OpenAPIDocsOptions` to the interpreter: @@ -258,43 +302,6 @@ import sttp.tapir._ val acceptHeader: EndpointInput[String] = header[String]("Accept").schema(_.hidden(true)) ``` -## Exposing generated OpenAPI documentation - -Exposing the OpenAPI can be done using [Swagger UI](https://swagger.io/tools/swagger-ui/) or -[Redoc](https://github.com/Redocly/redoc). You can either both interpret endpoints to OpenAPI's yaml and expose -them in a single step (see above), or you can do that separately. - -The modules `tapir-swagger-ui` and `tapir-redoc` contain server endpoint definitions, which given the documentation in -yaml format, will expose it using the given context path. To use, add as a dependency either -`tapir-swagger-ui`: -```scala -"com.softwaremill.sttp.tapir" %% "tapir-swagger-ui" % "@VERSION@" -``` - -or `tapir-redoc`: -```scala -"com.softwaremill.sttp.tapir" %% "tapir-redoc" % "@VERSION@" -``` - -Then, you'll need to pass the server endpoints to your server interpreter. For example, using akka-http: - -```scala mdoc:compile-only -import sttp.apispec.openapi.circe.yaml._ -import sttp.tapir._ -import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter -import sttp.tapir.server.akkahttp.AkkaHttpServerInterpreter -import sttp.tapir.swagger.SwaggerUI - -import scala.concurrent.Future -import scala.concurrent.ExecutionContext.Implicits.global - -val myEndpoints: Seq[AnyEndpoint] = ??? -val docsAsYaml: String = OpenAPIDocsInterpreter().toOpenAPI(myEndpoints, "My App", "1.0").toYaml - -// add to your akka routes -val swaggerUIRoute = AkkaHttpServerInterpreter().toRoute(SwaggerUI[Future](docsAsYaml)) -``` - ## Using SwaggerUI with sbt-assembly The `tapir-swagger-ui` and `tapir-swagger-ui-bundle` modules rely on a file in the `META-INF` directory tree, to diff --git a/doc/endpoint/basics.md b/doc/endpoint/basics.md index fd33f4696a..fe855ce72a 100644 --- a/doc/endpoint/basics.md +++ b/doc/endpoint/basics.md @@ -10,12 +10,11 @@ An endpoint is represented as a value of type `Endpoint[A, I, E, O, R]`, where: Input/output parameters (`A`, `I`, `E` and `O`) can be: -* of type `Unit`, when there's no input/ouput of the given type +* of type `Unit`, when there's no input/output * a single type * a tuple of types -Hence, an empty, initial endpoint (`tapir.endpoint`), with no inputs and no outputs, from which all other endpoints are -derived has the type: +Hence, an empty, initial endpoint, with no inputs and no outputs, from which all other endpoints are derived has the type: ```scala mdoc:compile-only import sttp.tapir._ @@ -31,7 +30,7 @@ import sttp.tapir._ type PublicEndpoint[I, E, O, -R] = Endpoint[Unit, I, E, O, R] ``` -A public endpoint which accepts two parameters of types `UUID` and `Int`, upon error returns a `String`, and on normal +A public endpoint has two inputs of types `UUID` and `Int`, upon error returns a `String`, and on normal completion returns a `User`, would have the type: ```scala mdoc:invisible @@ -47,13 +46,16 @@ val userEndpoint: PublicEndpoint[(UUID, Int), String, User, Any] = ??? ``` You can think of an endpoint as a function, which takes input parameters of type `A` & `I` and returns a result of type -`Either[E, O]`, where inputs or outputs can contain streaming bodies of type `S`. +`Either[E, O]`. ### Infallible endpoints Note that the empty `endpoint` description maps no values to either error and success outputs, however errors -are still represented and allowed to occur. If you preferred to use an endpoint description, where -errors can not happen, use `infallibleEndpoint: PublicEndpoint[Unit, Nothing, Unit, Nothing]`. This might be useful when +are still represented and allowed to occur. In case of the error output, the single member of the unit type, `(): Unit`, +maps to an empty-body `400 Bad Request`. + +If you prefer to use an endpoint description, where errors cannot happen, use +`infallibleEndpoint: PublicEndpoint[Unit, Nothing, Unit, Any]`. This might be useful when interpreting endpoints [as a client](../client/sttp.md). ## Defining an endpoint diff --git a/doc/endpoint/codecs.md b/doc/endpoint/codecs.md index 82e61178cc..b4a9cd303a 100644 --- a/doc/endpoint/codecs.md +++ b/doc/endpoint/codecs.md @@ -4,20 +4,39 @@ A `Codec[L, H, CF]` is a bi-directional mapping between low-level values of type Low level values are formatted as `CF`. A codec also contains the schema of the high-level value, which is used for validation and documentation. -There are built-in codecs for most common types such as `String`, `Int` etc. Codecs are usually defined as implicit -values and resolved implicitly when they are referenced. However, they can also be provided explicitly as needed. +For example, a `Codec[String, User, CodecFormat.Json]` contains: +* a function to decode a `String` into `User`, which assumes that the string if formatted as JSON; this decoding might fail of course in case of malformed input +* a function to encode a `User` into a JSON `String`; this encoding step cannot fail -For example, a `query[Int]("quantity")` specifies an input parameter which corresponds to the `quantity` query +There are built-in implicit codecs for most common types such as `String`, `Int`, `Instant` etc., as well as some +types representing header values. Take a look at the `Codec` companion object for a full list. The companion object +also contains a number of helper methods to create custom codecs. + +## Looking up codecs + +For most inputs/outputs, the appropriate codec is required as an implicit parameter. Hence codec instances are usually +defined as implicit values and resolved implicitly when they are referenced. However, they can also be provided +explicitly as needed. + +As an example, a `query[Int]("quantity")` specifies an input parameter which corresponds to the `quantity` query parameter and will be mapped as an `Int`. A query input requires a codec, where the low-level value is a `List[String]` (representing potentially 0, one, or multiple parameters with the given name in the URL). Hence, an implicit `Codec[List[String], Int, TextPlain]` value will be looked up when using the `query` method (which is defined in the `sttp.tapir` package). In this example, the codec will verify that there's a single query parameter with the given name, and parse it as an -integer. If any of this fails, a failure will be reported. +integer. If any of this fails, a decode failure will be reported. + +However, in some cases codecs aren't looked up as implicit values, instead being created from simpler components, which +themselves are looked up as implicits. This is the case e.g. for json bodies specified using `jsonBody`. The rationale +behind such a design is that this provides better error reporting, in case the implicit components, used to create the +codec, are missing. Consult the signature of the specific input/output to learn what are its implicit requirements. + +## Decode failures In a server setting, if the value cannot be parsed as an int, a decoding failure is reported, and the endpoint -won't match the request, or a `400 Bad Request` response is returned (depending on configuration). +won't match the request, or a `400 Bad Request` response is returned (depending on configuration). Take a look at +[server error handling](../server/errors.md) for more details. ## Optional and multiple parameters diff --git a/doc/endpoint/customtypes.md b/doc/endpoint/customtypes.md index a28605c609..6e04551116 100644 --- a/doc/endpoint/customtypes.md +++ b/doc/endpoint/customtypes.md @@ -1,8 +1,10 @@ # Custom types -To support a custom type, you'll need to provide an implicit `Codec` for that type. +To support a custom type, you'll need to provide an implicit `Codec` for that type, or the components to create such +a codec. The below mostly applies to wrapper types for inputs/outputs such as query parameters, path segments or +headers. For custom types used in [json](json.md) or [forms](forms.md) bodies, see the dedicated sections. -This can be done by writing a codec from scratch, mapping over an existing codec, or automatically deriving one. +A custom codec can be created by writing one from scratch, mapping over an existing codec, or automatically deriving one. Which of these approaches can be taken, depends on the context in which the codec will be used. ## Creating an implicit codec by hand diff --git a/doc/endpoint/integrations.md b/doc/endpoint/integrations.md index 16e476c110..67b8583849 100644 --- a/doc/endpoint/integrations.md +++ b/doc/endpoint/integrations.md @@ -1,5 +1,13 @@ # Datatypes integrations +```eval_rst +.. note:: + + Note that the codecs defined by the tapir integrations are used only when the specific types (e.g. enumerations0 are + used at the top level. Any nested usages (e.g. as part of a json body), need to be separately configured to work with + the used json library. +``` + ## Cats datatypes integration The `tapir-cats` module contains additional instances for some [cats](https://typelevel.org/cats/) diff --git a/doc/endpoint/ios.md b/doc/endpoint/ios.md index 2c791539f8..b13a8adcef 100644 --- a/doc/endpoint/ios.md +++ b/doc/endpoint/ios.md @@ -12,8 +12,9 @@ The `tapir` package contains a number of convenience methods to define an input For inputs, these are: * `path[T]`, which captures a path segment as an input parameter of type `T` -* any string, which will be implicitly converted to a fixed path segment. Path segments can be combined with the `/` - method, and don't map to any values (have type `EndpointInput[Unit]`) +* any string, which will be implicitly converted to a fixed path segment. Constant path segments can be combined with + the `/` method, and don't map to any values (they have type `EndpointInput[Unit]`, but they still modify the + endpoint's behavior) * `paths`, which maps to the whole remaining path as a `List[String]` * `query[T](name)` captures a query parameter with the given name * `queryParams` captures all query parameters, represented as `QueryParams` @@ -94,7 +95,7 @@ val statusEndpoint: PublicEndpoint[Unit, ErrorInfo, Status, Any] = baseEndpoint.in("status").out(jsonBody[Status]) ``` -The above endpoint will correspond to the `api/v1.0/status` path. +The above endpoint will correspond to the `/api/v1.0/status` path. ## Mapping over input/output values @@ -119,7 +120,7 @@ Next, you can use `mapDecode[II](f: I => DecodeResult[II])(g: II => I)`, to hand low-level value to a higher-value one) can fail. There's a couple of failure reasons, captured by the alternatives of the `DecodeResult` trait. -Mappings can also be done given an `Mapping[I, II]` instance. More on that in the secion on [codecs](codecs.md). +Mappings can also be done given a `Mapping[I, II]` instance. More on that in the section on [codecs](codecs.md). Creating a mapping between a tuple and a case class is a common operation, hence there's also a `mapTo[CaseClass]` method, which automatically provides the functions to construct/deconstruct the case class: @@ -226,7 +227,8 @@ To match a path prefix, first define inputs which match the path prefix, and the ### Arbitrary status codes To provide a (varying) status code of a server response, use the `statusCode` output, which maps to a value of type -`sttp.model.StatusCode`. The companion object contains known status codes as constants. This type of output is used only +`sttp.model.StatusCode`. In a server setting, the specific status code will then have to be provided dynamically by the +server logic. The companion object contains known status codes as constants. This type of output is used only when interpreting the endpoint as a server. If your endpoint returns varying status codes which you would like to have listed in documentation use `statusCode.description(code1, "code1 description").description(code2, "code2 description")` output. @@ -240,6 +242,12 @@ A fixed status code can be specified using the `statusCode(code)` output. Unless specified otherwise, successful responses are returned with the `200 OK` status code, and errors with `400 Bad Request`. For exception and decode failure handling, see [error handling](../server/errors.md). +### Different outputs with different status codes + +If you'd like to return different content together with a varying status code, use a [oneOf](oneof.md) output. +Each output variant can be paired with a fixed status code output (`statusCode(code)`), or a varying one, which will +be determined dynamically by the server logic. + ## Selected inputs/outputs for non-standard types * some header values can be decoded into a more structured representation, e.g. `header[MediaType]`, `header[ETag]`, diff --git a/doc/endpoint/json.md b/doc/endpoint/json.md index 6bbc9a049f..b709cc61a5 100644 --- a/doc/endpoint/json.md +++ b/doc/endpoint/json.md @@ -1,12 +1,25 @@ # Working with JSON Json values are supported through codecs, which encode/decode values to json strings. Most often, you'll be using a -third-party library to perform the actual json parsing/printing. Currently, [zio-json](https://github.com/zio/zio-json), [Circe](https://github.com/circe/circe), [µPickle](http://www.lihaoyi.com/upickle/), [Spray JSON](https://github.com/spray/spray-json), [Play JSON](https://github.com/playframework/play-json), [Tethys JSON](https://github.com/tethys-json/tethys), [Jsoniter-scala](https://github.com/plokhotnyuk/jsoniter-scala), and [Json4s](https://github.com/json4s/json4s) are supported. +third-party library to perform the actual json parsing/printing. See below for the list of supported libraries. -All of the integrations, when imported into scope, define a `jsonBody[T]` method. This method depends on -library-specific implicits being in scope, and derives from them a json codec. The derivation also requires implicit -`Schema[T]` and `Validator[T]` instances, which should be automatically derived. For more details see documentation -on supporting [custom types](customtypes.md). +All the integrations, when imported into scope, define a `jsonBody[T]` method. + +Instead of providing the json codec as an implicit value, this method depends on library-specific implicits being in +scope, and basing on these values creates a json codec. The derivation also requires +an implicit `Schema[T]` instance, which can be automatically derived. For more details see sections on +[schema derivation](schemas.md) and on supporting [custom types](customtypes.md) in general. Such a design provides +better error reporting, in case one of the components required to create the json codec is missing. + +```eval_rst +.. note:: + + Note that the process of deriving schemas, and deriving library-specific json encoders and decoders is entirely + separate. The first is controlled by tapir, the second - by the json library. Any customisation, e.g. for field + naming or inheritance strategies, must be done separately for both derivations. +``` + +## Implicit json codecs If you have a custom, implicit `Codec[String, T, Json]` instance, you should use the `customJsonBody[T]` method instead. This description of endpoint input/output, instead of deriving a codec basing on other library-specific implicits, uses @@ -14,7 +27,7 @@ the json codec that is in scope. ## Circe -To use Circe, add the following dependency to your project: +To use [Circe](https://github.com/circe/circe), add the following dependency to your project: ```scala "com.softwaremill.sttp.tapir" %% "tapir-json-circe" % "@VERSION@" @@ -26,15 +39,12 @@ Next, import the package (or extend the `TapirJsonCirce` trait, see [MyTapir](.. import sttp.tapir.json.circe._ ``` -This will allow automatically deriving `Codec`s which, given an in-scope circe `Encoder`/`Decoder` and a `Schema`, -will create a codec using the json media type. Circe includes a couple of approaches to generating encoders/decoders +The above import brings into scope the `jsonBody[T]` body input/output description, which creates a codec, given an +in-scope circe `Encoder`/`Decoder` and a `Schema`. Circe includes a couple of approaches to generating encoders/decoders (manual, semi-auto and auto), so you may choose whatever suits you. Note that when using Circe's auto derivation, any encoders/decoders for custom types must be in scope as well. -Additionally, the above import brings into scope the `jsonBody[T]` body input/output description, which uses the above -codec. - For example, to automatically generate a JSON codec for a case class: ```scala mdoc:compile-only @@ -48,6 +58,8 @@ case class Book(author: String, title: String, year: Int) val bookInput: EndpointIO[Book] = jsonBody[Book] ``` +### Configuring the circe printer + Circe lets you select an instance of `io.circe.Printer` to configure the way JSON objects are rendered. By default Tapir uses `Printer.nospaces`, which would render: @@ -88,7 +100,7 @@ Now the above JSON object will render as ## µPickle -To use µPickle add the following dependency to your project: +To use [µPickle](http://www.lihaoyi.com/upickle/) add the following dependency to your project: ```scala "com.softwaremill.sttp.tapir" %% "tapir-json-upickle" % "@VERSION@" @@ -100,7 +112,7 @@ Next, import the package (or extend the `TapirJsonuPickle` trait, see [MyTapir]( import sttp.tapir.json.upickle._ ``` -µPickle requires a ReadWriter in scope for each type you want to serialize. In order to provide one use the `macroRW` macro in the companion object as follows: +µPickle requires a `ReadWriter` in scope for each type you want to serialize. In order to provide one use the `macroRW` macro in the companion object as follows: ```scala mdoc:compile-only import sttp.tapir._ @@ -123,7 +135,7 @@ For more examples, including making a custom encoder/decoder, see [TapirJsonuPic ## Play JSON -To use Play JSON add the following dependency to your project: +To use [Play JSON](https://github.com/playframework/play-json) add the following dependency to your project: ```scala "com.softwaremill.sttp.tapir" %% "tapir-json-play" % "@VERSION@" @@ -139,7 +151,7 @@ Play JSON requires `Reads` and `Writes` implicit values in scope for each type y ## Spray JSON -To use Spray JSON add the following dependency to your project: +To use [Spray JSON](https://github.com/spray/spray-json) add the following dependency to your project: ```scala "com.softwaremill.sttp.tapir" %% "tapir-json-spray" % "@VERSION@" @@ -155,7 +167,7 @@ Spray JSON requires a `JsonFormat` implicit value in scope for each type you wan ## Tethys JSON -To use Tethys JSON add the following dependency to your project: +To use [Tethys JSON](https://github.com/tethys-json/tethys) add the following dependency to your project: ```scala "com.softwaremill.sttp.tapir" %% "tapir-json-tethys" % "@VERSION@" @@ -185,7 +197,6 @@ import sttp.tapir.json.jsoniter._ Jsoniter Scala requires `JsonValueCodec` implicit value in scope for each type you want to serialize. - ## Json4s To use [json4s](https://github.com/json4s/json4s) add the following dependencies to your project: @@ -219,7 +230,7 @@ implicit val formats: Formats = org.json4s.jackson.Serialization.formats(NoTypeH ## Zio JSON -To use Zio JSON, add the following dependency to your project: +To use [zio-json](https://github.com/zio/zio-json), add the following dependency to your project: ```scala "com.softwaremill.sttp.tapir" %% "tapir-json-zio" % "@VERSION@" @@ -238,14 +249,6 @@ To add support for additional JSON libraries, see the [sources](https://github.com/softwaremill/tapir/blob/master/json/circe/src/main/scala/sttp/tapir/json/circe/TapirJsonCirce.scala) for the Circe codec (which is just a couple of lines of code). -## Schemas - -To derive json codecs automatically, not only implicits from the base library are needed (e.g. a circe -`Encoder`/`Decoder`), but also an implicit `Schema[T]` value, which provides a mapping between a type `T` and its -schema. A schema-for value contains a single `schema: Schema` field. - -See [custom types](customtypes.md) for details. - ## Next Read on about [working with forms](forms.md). diff --git a/doc/endpoint/schemas.md b/doc/endpoint/schemas.md index 932a2ce4ed..078b757856 100644 --- a/doc/endpoint/schemas.md +++ b/doc/endpoint/schemas.md @@ -1,5 +1,7 @@ # Schema derivation +A schema describes the shape of a value, how the low-level representation should be structured. + Implicit schemas for basic types (`String`, `Int`, etc.), and their collections (`Option`, `List`, `Array` etc.) are defined out-of-the box. They don't contain any meta-data, such as descriptions or example values. @@ -65,7 +67,6 @@ values must be `lazy val`s. even when otherwise auto derivation is used. ``` - ## Derivation for recursive types in Scala3 In Scala3, any schemas for recursive types need to be provided as typed `implicit def` (not a `given`)! diff --git a/doc/endpoint/validation.md b/doc/endpoint/validation.md index f41686cedd..00128994a1 100644 --- a/doc/endpoint/validation.md +++ b/doc/endpoint/validation.md @@ -2,7 +2,11 @@ Tapir supports validation for primitive types. Validation of composite values, whole data structures, business rules enforcement etc. should be done as part of the [server logic](../server/logic.md) of the endpoint, using the -dedicated error output (the `E` in `Endpoint[I, E, O, S]`) to report errors. +dedicated error output (the `E` in `Endpoint[A, I, E, O, S]`) to report errors. + +For some guidelines as to where to perform a specific type of validation, see the ["Validation analysis paralysis"](https://blog.softwaremill.com/validation-analysis-paralysis-ca9bdef0a6d7) article. A good indicator as to where to place particular validation +logic might be if the property that we are checking is a format error, or a business-level error? The validation +capabilities described in this section are intended only for format errors. ## Single type validation diff --git a/doc/examples.md b/doc/examples.md index 9bfa044df4..5087883fd6 100644 --- a/doc/examples.md +++ b/doc/examples.md @@ -1,8 +1,8 @@ # 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/examples/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 [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. -## Other examples +## Third-party examples To see an example project using tapir, [check out this Todo-Backend](https://github.com/hejfelix/tapir-http4s-todo-mvc) using tapir and http4s. @@ -12,12 +12,16 @@ A new project can be created using: `sbt new https://codeberg.org/wegtam/http4s- ## Blogs, articles +* [Security improvements in tapir 0.19](https://softwaremill.com/security-improvements-in-tapir-0-19/) +* [Tapir serverless: a proof of concept](https://blog.softwaremill.com/tapir-serverless-a-proof-of-concept-6b8c9de4d396) * [Designing tapir's WebSockets support](https://blog.softwaremill.com/designing-tapirs-websockets-support-ff1573166368) * [Three easy endpoints](https://blog.softwaremill.com/three-easy-endpoints-a6cbd52b0a6e) -* [tAPIr’s Endpoint meets ZIO’s IO](https://blog.softwaremill.com/tapirs-endpoint-meets-zio-s-io-3278099c5e10) +* [tAPIr's Endpoint meets ZIO's IO](https://blog.softwaremill.com/tapirs-endpoint-meets-zio-s-io-3278099c5e10) * [Describe, then interpret: HTTP endpoints using tapir](https://blog.softwaremill.com/describe-then-interpret-http-endpoints-using-tapir-ac139ba565b0) * [Functional pancakes](https://blog.softwaremill.com/functional-pancakes-cf70023f0dcb) ## Videos +* [ScalaLove 2020: Your HTTP endpoints are data, as well!](https://www.youtube.com/watch?v=yuQNgZgSFIc&t=944s) +* [Scalar 2020: A Functional Scala Stack For 2020](https://www.youtube.com/watch?v=DGlkap5kzGU) * [ScalaWorld 2019: Designing Programmer-Friendly APIs](https://www.youtube.com/watch?v=I3loMuHnYqw) diff --git a/doc/index.md b/doc/index.md index 0167f85cba..f47b77207a 100644 --- a/doc/index.md +++ b/doc/index.md @@ -1,13 +1,6 @@ -# tapir, or Typed API descRiptions +# tapir -## Why tapir? - -* **type-safety**: compile-time guarantees, develop-time completions, read-time information -* **declarative**: separate the shape of the endpoint (the "what"), from the server logic (the "how") -* **OpenAPI / Swagger integration**: generate documentation from endpoint descriptions -* **observability**: leverage the metadata to report rich metrics and tracing information -* **abstraction**: re-use common endpoint definitions, as well as individual inputs/outputs -* **library, not a framework**: integrates with your stack +Declarative, type-safe web endpoints library. ## Intro @@ -34,16 +27,28 @@ input and output parameters. An endpoint specification can be interpreted as: * [OpenAPI](docs/openapi.md) * [AsyncAPI](docs/asyncapi.md) -Tapir is licensed under Apache2, the source code is [available on GitHub](https://github.com/softwaremill/tapir). - Depending on how you prefer to explore the library, take a look at one of the [examples](examples.md) or read on for a more detailed description of how tapir works! +## Why tapir? + +* **type-safety**: compile-time guarantees, develop-time completions, read-time information +* **declarative**: separate the shape of the endpoint (the "what"), from the server logic (the "how") +* **OpenAPI / Swagger integration**: generate documentation from endpoint descriptions +* **observability**: leverage the metadata to report rich metrics and tracing information +* **abstraction**: re-use common endpoint definitions, as well as individual inputs/outputs +* **library, not a framework**: integrates with your stack + +## Availability + Tapir is available: * all modules - Scala 2.12 and 2.13 on the JVM -* selected modules (core; http4s, vertx, netty, aws servers; sttp and http4s clients; openapi; some js and datatype integrations) - Scala 3 on the JVM -* selected modules (aws server; sttp client; some js and datatype integrations) - Scala 2.12, 2.13 and 3 using Scala.JS. +* selected modules - Scala 3 on the JVM +* selected modules - Scala 2.12, 2.13 and 3 using Scala.JS +* selected modules - Scala 2.12, 2.13 and 3 using Scala Native + +Tapir is licensed under Apache2, the source code is [available on GitHub](https://github.com/softwaremill/tapir). ## Adopters @@ -51,7 +56,7 @@ Is your company already using tapir? We're continually expanding the "adopters" Please email us at [tapir@softwaremill.com](mailto:tapir@softwaremill.com) from your company's email with a link to your logo (if we can use it, of course!) or with details who to kindly ask for permission to feature the logo in tapir's documentation. We'll handle the rest. -We're seeing tapir's download numbers going steadily up; as we're nearing 1.0, the additional confidence boost for newcomers will help us to build tapir's ecosystem and make it thrive. Thank you! :) +Thank you!
Adobe @@ -84,16 +89,16 @@ import io.circe.generic.auto._ type Limit = Int type AuthToken = String -case class BooksFromYear(genre: String, year: Int) +case class BooksQuery(genre: String, year: Int) case class Book(title: String) // Define an endpoint -val booksListing: PublicEndpoint[(BooksFromYear, Limit, AuthToken), String, List[Book], Any] = +val booksListing: PublicEndpoint[(BooksQuery, Limit, AuthToken), String, List[Book], Any] = endpoint .get - .in(("books" / path[String]("genre") / path[Int]("year")).mapTo[BooksFromYear]) + .in(("books" / path[String]("genre") / path[Int]("year")).mapTo[BooksQuery]) .in(query[Limit]("limit").description("Maximum number of books to retrieve")) .in(header[AuthToken]("X-Auth-Token")) .errorOut(stringBody) @@ -116,7 +121,7 @@ import akka.http.scaladsl.server.Route import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits.global -def bookListingLogic(bfy: BooksFromYear, +def bookListingLogic(bfy: BooksQuery, limit: Limit, at: AuthToken): Future[Either[String, List[Book]]] = Future.successful(Right(List(Book("The Sorrows of Young Werther")))) @@ -133,7 +138,7 @@ import sttp.client3._ val booksListingRequest: Request[DecodeResult[Either[String, List[Book]]], Any] = SttpClientInterpreter() .toRequest(booksListing, Some(uri"http://localhost:8080")) - .apply((BooksFromYear("SF", 2016), 20, "xyz-abc-123")) + .apply((BooksQuery("SF", 2016), 20, "xyz-abc-123")) ``` ## Other sttp projects @@ -143,6 +148,8 @@ sttp is a family of Scala HTTP-related projects, and currently includes: * [sttp client](https://github.com/softwaremill/sttp): the Scala HTTP client you always wanted! * sttp tapir: this project * [sttp model](https://github.com/softwaremill/sttp-model): simple HTTP model classes (used by client & tapir) +* [sttp shared](https://github.com/softwaremill/sttp-shared): shared web socket, FP abstractions, capabilities and streaming code. +* [sttp apispec](https://github.com/softwaremill/sttp-apispec): OpenAPI, AsyncAPI and JSON Schema models. ## Sponsors @@ -150,7 +157,7 @@ Development and maintenance of sttp tapir is sponsored by [SoftwareMill](https:/ [![](https://files.softwaremill.com/logo/logo.png "SoftwareMill")](https://softwaremill.com) -# Table of contents +## Table of contents ```eval_rst .. toctree:: @@ -159,7 +166,7 @@ Development and maintenance of sttp tapir is sponsored by [SoftwareMill](https:/ quickstart examples - goals + stability .. toctree:: :maxdepth: 2 @@ -237,5 +244,6 @@ Development and maintenance of sttp tapir is sponsored by [SoftwareMill](https:/ mytapir troubleshooting migrating + goals contributing diff --git a/doc/quickstart.md b/doc/quickstart.md index 33bd26d9a3..a7d80fc392 100644 --- a/doc/quickstart.md +++ b/doc/quickstart.md @@ -16,14 +16,6 @@ you import the main package entirely, i.e.: import sttp.tapir._ ``` -Partial unification is now enabled by default from Scala 2.13. However, if you're using Scala 2.12 or older, and don't -have it already, you'll want to to enable partial unification in the compiler (alternatively, you'll need to manually -provide type arguments in some cases). In sbt, this is: - -```scala -scalacOptions += "-Ypartial-unification" -``` - Finally, type: ```scala @@ -31,3 +23,13 @@ endpoint. ``` and see where auto-complete gets you! + +## Scala 2.12 + +Partial unification is now enabled by default from Scala 2.13. However, if you're using Scala 2.12 or older, and don't +have it already, you'll want to to enable partial unification in the compiler (alternatively, you'll need to manually +provide type arguments in some cases). In sbt, this is: + +```scala +scalacOptions += "-Ypartial-unification" +``` \ No newline at end of file diff --git a/doc/server/interceptors.md b/doc/server/interceptors.md index a8054440a9..7fb523bedb 100644 --- a/doc/server/interceptors.md +++ b/doc/server/interceptors.md @@ -29,6 +29,7 @@ The following interceptors are used by default, and if enabled, called in this o * exception interceptor * logging interceptor +* unsupported media type interceptor * decode failure handler interceptor Note that while the request will be passed top-to-bottom, handling of the result will be done in opposite order. @@ -38,4 +39,11 @@ only later passed to the exception interceptor. Using `customiseInterceptors` on the options companion object, it is possible to customise the built-in interceptors. New ones can be prepended to the interceptor stack using `.prependInterceptor`, added before the decode failure interceptor using `.addInterceptor`, or appended using `.appendInterceptor`. Customisation can include removing the interceptor -altogether. \ No newline at end of file +altogether. + +## Attributes + +When implementing interceptors, it might be useful to take advantage of attributes, which can be attached both to +requests, as well as endpoint descriptions. Attributes are keyed using an `AttributeKey`. Typically, each attribute +corresponds to a unique type, and the key instance for that type can be created using `AttributeKey[T]`. The attribute +values then have to be of the given type `T`. \ No newline at end of file diff --git a/doc/server/vertx.md b/doc/server/vertx.md index d89ebfab49..8b890d9294 100644 --- a/doc/server/vertx.md +++ b/doc/server/vertx.md @@ -180,7 +180,7 @@ object Short extends ZIOAppDefault { .in(query[String]("key")) .out(plainBody[String]) - val attach = VertxZioServerInterpreter().route(responseEndpoint.zServerLogic { key => UIO.succeed(key) }) + val attach = VertxZioServerInterpreter().route(responseEndpoint.zServerLogic { key => ZIO.succeed(key) }) override def run = { ZIO.scoped( diff --git a/doc/server/zio-http4s.md b/doc/server/zio-http4s.md index 460ab8a017..df19b443f7 100644 --- a/doc/server/zio-http4s.md +++ b/doc/server/zio-http4s.md @@ -142,7 +142,7 @@ val wsRoutes: WebSocketBuilder2[Task] => HttpRoutes[Task] = val serve: Task[Unit] = BlazeServerBuilder[Task] - .withExecutionContext(runtime.runtimeConfig.executor.asExecutionContext) + .withExecutionContext(runtime.executor.asExecutionContext) .bindHttp(8080, "localhost") .withHttpWebSocketApp(wsb => Router("/" -> wsRoutes(wsb)).orNotFound) .serve @@ -164,14 +164,14 @@ import sttp.tapir.PublicEndpoint import sttp.tapir.ztapir._ import org.http4s.HttpRoutes import zio.{Task, ZIO} -import zio.stream.Stream +import zio.stream.{Stream, ZStream} val sseEndpoint: PublicEndpoint[Unit, Unit, Stream[Throwable, ServerSentEvent], ZioStreams] = endpoint.get.out(serverSentEventsBody) val routes: HttpRoutes[Task] = ZHttp4sServerInterpreter() - .from(sseEndpoint.zServerLogic(_ => ZIO.succeed(Stream(ServerSentEvent(Some("data"), None, None, None))))) + .from(sseEndpoint.zServerLogic(_ => ZIO.succeed(ZStream(ServerSentEvent(Some("data"), None, None, None))))) .toRoutes ``` diff --git a/doc/stability.md b/doc/stability.md new file mode 100644 index 0000000000..4e735def70 --- /dev/null +++ b/doc/stability.md @@ -0,0 +1,100 @@ +# Stability of modules + +The modules are categorised using the following levels: + +* **stable**: binary compatibility is guaranteed across major versions; adheres to semantic versioning +* **stabilising**: the API is mostly stable, with rare binary-incompatible changes possible in minor releases (only if necessary) +* **experimental**: API can change significantly even in patch releases + +## Main modules + +| Module | Level | +|----------------|-------------| +| core (Scala 2) | stabilising | +| core (Scala 3) | stabilising | +| server-core | stabilising | +| client-core | stabilising | + +## Server interpreters + +| Module | Level | +|-----------|--------------| +| akka-http | stabilising | +| armeria | stabilising | +| finatra | stabilising | +| http4s | stabilising | +| netty | experimental | +| play | stabilising | +| vertx | stabilising | +| zio1-http | experimental | +| zio-http | experimental | + +## Client interpreters + +| Module | Level | +|--------|-------------| +| sttp | stabilising | +| play | stabilising | +| http4s | stabilising | + +## Documentation interpreters + +| Module | Level | +|----------|-------------| +| openapi | stabilising | +| asyncapi | stabilising | + +## Serverless interpreters + +| Module | Level | +|---------------|--------------| +| aws-lambda | experimental | +| aws-sam | experimental | +| aws-terraform | experimental | + +## Integration modules + +| Module | Level | +|------------|--------------| +| cats | stabilising | +| derevo | stabilising | +| enumeratum | stabilising | +| newtype | stabilising | +| refined | stabilising | +| zio | experimental | +| zio1 | stabilising | + +## JSON modules + +| Module | Level | +|------------|--------------| +| circe | stabilising | +| json4s | stabilising | +| jsoniter | stabilising | +| play-json | stabilising | +| spray-json | stabilising | +| tethys | stabilising | +| upickle | stabilising | +| zio-json | experimental | +| zio1-json | experimental | + +## Testing modules + +| Module | Level | +|-----------|--------------| +| testing | stabilising | +| sttp-mock | experimental | +| sttp-stub | stabilising | + +## Observability modules + +| Module | Level | +|-----------------------|-------------| +| opentelemetry-metrics | stabilising | +| prometheus-metrics | stabilising | + +## Other modules + +| Module | Level | +|--------------------|--------------| +| openapi-codegen | experimental | diff --git a/examples/src/main/scala/sttp/tapir/examples/HelloWorldTCPNettyCatsServer.scala b/examples/src/main/scala/sttp/tapir/examples/HelloWorldNettyCatsServer.scala similarity index 95% rename from examples/src/main/scala/sttp/tapir/examples/HelloWorldTCPNettyCatsServer.scala rename to examples/src/main/scala/sttp/tapir/examples/HelloWorldNettyCatsServer.scala index d03290f61e..62bd0665b2 100644 --- a/examples/src/main/scala/sttp/tapir/examples/HelloWorldTCPNettyCatsServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/HelloWorldNettyCatsServer.scala @@ -3,13 +3,12 @@ package sttp.tapir.examples import cats.effect.IO import sttp.client3.{HttpURLConnectionBackend, Identity, SttpBackend, UriContext, asStringAlways, basicRequest} import sttp.model.StatusCode -import sttp.tapir.server.netty.NettyServerType import sttp.tapir.{PublicEndpoint, endpoint, query, stringBody} import cats.effect.unsafe.implicits.global import sttp.tapir.server.netty.NettyServerType._ import sttp.tapir.server.netty.cats.{NettyCatsServer, NettyCatsServerBinding} -object HelloWorldTCPNettyCatsServer extends App { +object HelloWorldNettyCatsServer extends App { // One endpoint on GET /hello with query parameter `name` val helloWorldEndpoint: PublicEndpoint[String, Unit, String, Any] = endpoint.get.in("hello").in(query[String]("name")).out(stringBody) diff --git a/examples/src/main/scala/sttp/tapir/examples/HelloWorldTCPNettyFutureServer.scala b/examples/src/main/scala/sttp/tapir/examples/HelloWorldNettyFutureServer.scala similarity index 97% rename from examples/src/main/scala/sttp/tapir/examples/HelloWorldTCPNettyFutureServer.scala rename to examples/src/main/scala/sttp/tapir/examples/HelloWorldNettyFutureServer.scala index 82bfd804e9..d0c03a98d6 100644 --- a/examples/src/main/scala/sttp/tapir/examples/HelloWorldTCPNettyFutureServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/HelloWorldNettyFutureServer.scala @@ -10,7 +10,7 @@ import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration.Duration import scala.concurrent.{Await, Future} -object HelloWorldTCPNettyFutureServer extends App { +object HelloWorldNettyFutureServer extends App { // One endpoint on GET /hello with query parameter `name` val helloWorldEndpoint: PublicEndpoint[String, Unit, String, Any] = endpoint.get.in("hello").in(query[String]("name")).out(stringBody) diff --git a/examples/src/main/scala/sttp/tapir/examples/MultipleServerEndpointsAkkaServer.scala b/examples/src/main/scala/sttp/tapir/examples/MultipleServerEndpointsAkkaServer.scala deleted file mode 100644 index 477686e991..0000000000 --- a/examples/src/main/scala/sttp/tapir/examples/MultipleServerEndpointsAkkaServer.scala +++ /dev/null @@ -1,40 +0,0 @@ -package sttp.tapir.examples - -import akka.actor.ActorSystem -import akka.http.scaladsl.Http -import akka.http.scaladsl.server.Route -import sttp.client3._ -import sttp.tapir._ -import sttp.tapir.server.akkahttp.AkkaHttpServerInterpreter - -import scala.concurrent.duration._ -import scala.concurrent.{Await, Future} - -object MultipleServerEndpointsAkkaServer extends App { - implicit val actorSystem: ActorSystem = ActorSystem() - import actorSystem.dispatcher - - // endpoint descriptions, together with the server logic - val endpoint1 = endpoint.get.in("endpoint1").out(stringBody).serverLogicSuccess { _ => Future.successful("ok1") } - val endpoint2 = - endpoint.get.in("endpoint2").in(path[String]).out(stringBody).serverLogicSuccess { path => Future.successful(s"ok2: $path") } - - // converting the endpoints to a (single) route - val route: Route = AkkaHttpServerInterpreter().toRoute(List(endpoint1, endpoint2)) - - // starting the server - val bindAndCheck = Http().newServerAt("localhost", 8080).bindFlow(route).map { _ => - // testing - val backend: SttpBackend[Identity, Any] = HttpURLConnectionBackend() - - val result1: String = basicRequest.response(asStringAlways).get(uri"http://localhost:8080/endpoint1").send(backend).body - println("Got result (1): " + result1) - assert(result1 == "ok1") - - val result2: String = basicRequest.response(asStringAlways).get(uri"http://localhost:8080/endpoint2/apple").send(backend).body - println("Got result (2): " + result2) - assert(result2 == "ok2: apple") - } - - Await.result(bindAndCheck.transformWith { r => actorSystem.terminate().transform(_ => r) }, 1.minute) -} diff --git a/examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala index b46bdd2c10..e78d5a78c8 100644 --- a/examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala @@ -30,9 +30,9 @@ object ZioEnvExampleHttp4sServer extends ZIOAppDefault { override def find(petId: Int): IO[String, Pet] = { Console.printLine(s"Got request for pet: $petId").mapError(_.getMessage) zipRight { if (petId == 35) { - IO.succeed(Pet("Tapirus terrestris", "https://en.wikipedia.org/wiki/Tapir")) + ZIO.succeed(Pet("Tapirus terrestris", "https://en.wikipedia.org/wiki/Tapir")) } else { - IO.fail("Unknown pet id") + ZIO.fail("Unknown pet id") } } } @@ -62,7 +62,7 @@ object ZioEnvExampleHttp4sServer extends ZIOAppDefault { // Starting the server val serve: ZIO[PetService, Throwable, Unit] = { BlazeServerBuilder[RIO[PetService, *]] - .withExecutionContext(runtime.runtimeConfig.executor.asExecutionContext) + .withExecutionContext(runtime.executor.asExecutionContext) .bindHttp(8080, "localhost") .withHttpApp(Router("/" -> (petRoutes <+> swaggerRoutes)).orNotFound) .serve diff --git a/examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala index 3a2b9cdd25..38414a9819 100644 --- a/examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala @@ -3,16 +3,16 @@ package sttp.tapir.examples import cats.syntax.all._ import io.circe.generic.auto._ import org.http4s._ -import org.http4s.server.Router import org.http4s.blaze.server.BlazeServerBuilder +import org.http4s.server.Router import sttp.tapir.PublicEndpoint -import sttp.tapir.json.circe._ import sttp.tapir.generic.auto._ +import sttp.tapir.json.circe._ import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter import sttp.tapir.swagger.bundle.SwaggerInterpreter import sttp.tapir.ztapir._ -import zio.{ExitCode, IO, Task, UIO, URIO, ZIOAppDefault} import zio.interop.catz._ +import zio.{ExitCode, Task, URIO, ZIO, ZIOAppDefault} object ZioExampleHttp4sServer extends ZIOAppDefault { case class Pet(species: String, url: String) @@ -24,9 +24,9 @@ object ZioExampleHttp4sServer extends ZIOAppDefault { val petRoutes: HttpRoutes[Task] = ZHttp4sServerInterpreter() .from(petEndpoint.zServerLogic { petId => if (petId == 35) { - UIO.succeed(Pet("Tapirus terrestris", "https://en.wikipedia.org/wiki/Tapir")) + ZIO.succeed(Pet("Tapirus terrestris", "https://en.wikipedia.org/wiki/Tapir")) } else { - IO.fail("Unknown pet id") + ZIO.fail("Unknown pet id") } }) .toRoutes @@ -34,9 +34,9 @@ object ZioExampleHttp4sServer extends ZIOAppDefault { // Same as above, but combining endpoint description with server logic: val petServerEndpoint: ZServerEndpoint[Any, Any] = petEndpoint.zServerLogic { petId => if (petId == 35) { - UIO.succeed(Pet("Tapirus terrestris", "https://en.wikipedia.org/wiki/Tapir")) + ZIO.succeed(Pet("Tapirus terrestris", "https://en.wikipedia.org/wiki/Tapir")) } else { - IO.fail("Unknown pet id") + ZIO.fail("Unknown pet id") } } val petServerRoutes: HttpRoutes[Task] = ZHttp4sServerInterpreter().from(petServerEndpoint).toRoutes @@ -51,7 +51,7 @@ object ZioExampleHttp4sServer extends ZIOAppDefault { // Starting the server val serve: Task[Unit] = BlazeServerBuilder[Task] - .withExecutionContext(runtime.runtimeConfig.executor.asExecutionContext) + .withExecutionContext(runtime.executor.asExecutionContext) .bindHttp(8080, "localhost") .withHttpApp(Router("/" -> (petRoutes <+> swaggerRoutes)).orNotFound) .serve diff --git a/examples/src/main/scala/sttp/tapir/examples/ZioExampleZioHttpServer.scala b/examples/src/main/scala/sttp/tapir/examples/ZioExampleZioHttpServer.scala index 832aa808a1..6beec2999d 100644 --- a/examples/src/main/scala/sttp/tapir/examples/ZioExampleZioHttpServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/ZioExampleZioHttpServer.scala @@ -9,7 +9,7 @@ import sttp.tapir.swagger.bundle.SwaggerInterpreter import sttp.tapir.ztapir._ import zhttp.http.HttpApp import zhttp.service.Server -import zio.{IO, Task, UIO, ZIO, ZIOAppDefault} +import zio.{Task, ZIO, ZIOAppDefault} object ZioExampleZioHttpServer extends ZIOAppDefault { case class Pet(species: String, url: String) @@ -29,9 +29,9 @@ object ZioExampleZioHttpServer extends ZIOAppDefault { // Same as above, but combining endpoint description with server logic: val petServerEndpoint: ZServerEndpoint[Any, Any] = petEndpoint.zServerLogic { petId => if (petId == 35) { - UIO.succeed(Pet("Tapirus terrestris", "https://en.wikipedia.org/wiki/Tapir")) + ZIO.succeed(Pet("Tapirus terrestris", "https://en.wikipedia.org/wiki/Tapir")) } else { - IO.fail("Unknown pet id") + ZIO.fail("Unknown pet id") } } val petServerRoutes: HttpApp[Any, Throwable] = ZioHttpInterpreter().toHttp(List(petServerEndpoint)) diff --git a/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala b/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala index 339c13a21a..20de9a641a 100644 --- a/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala +++ b/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala @@ -54,7 +54,7 @@ object ZioPartialServerLogicHttp4s extends ZIOAppDefault { def assertEquals(at: Task[String], b: String): Task[Unit] = at.flatMap { a => - if (a == b) Task.succeed(()) else Task.fail(new IllegalArgumentException(s"$a was not equal to $b")) + if (a == b) ZIO.succeed(()) else ZIO.fail(new IllegalArgumentException(s"$a was not equal to $b")) } assertEquals(testWith("hello1", "Hello", "secret"), "Hello, Spock!") *> @@ -70,7 +70,7 @@ object ZioPartialServerLogicHttp4s extends ZIOAppDefault { override def run: URIO[Any, ExitCode] = BlazeServerBuilder[RIO[UserService, *]] - .withExecutionContext(runtime.runtimeConfig.executor.asExecutionContext) + .withExecutionContext(runtime.executor.asExecutionContext) .bindHttp(8080, "localhost") .withHttpApp(Router("/" -> helloWorldRoutes).orNotFound) .resource @@ -92,8 +92,8 @@ object UserAuthenticationLayer { val live: ZLayer[Any, Nothing, Service] = ZLayer.succeed(new Service { def auth(token: String): IO[Int, User] = { - if (token == "secret") IO.succeed(User("Spock")) - else IO.fail(AuthenticationErrorCode) + if (token == "secret") ZIO.succeed(User("Spock")) + else ZIO.fail(AuthenticationErrorCode) } }) diff --git a/examples/src/main/scala/sttp/tapir/examples/Http4sClientExample.scala b/examples/src/main/scala/sttp/tapir/examples/client/Http4sClientExample.scala similarity index 98% rename from examples/src/main/scala/sttp/tapir/examples/Http4sClientExample.scala rename to examples/src/main/scala/sttp/tapir/examples/client/Http4sClientExample.scala index a8edd68f2e..25ce30abc8 100644 --- a/examples/src/main/scala/sttp/tapir/examples/Http4sClientExample.scala +++ b/examples/src/main/scala/sttp/tapir/examples/client/Http4sClientExample.scala @@ -1,4 +1,4 @@ -package sttp.tapir.examples +package sttp.tapir.examples.client import cats.effect.{ExitCode, IO, IOApp} import com.typesafe.scalalogging.StrictLogging diff --git a/examples/src/main/scala/sttp/tapir/examples/BooksExampleSemiauto.scala b/examples/src/main/scala/sttp/tapir/examples/custom_types/BooksExampleSemiauto.scala similarity index 99% rename from examples/src/main/scala/sttp/tapir/examples/BooksExampleSemiauto.scala rename to examples/src/main/scala/sttp/tapir/examples/custom_types/BooksExampleSemiauto.scala index b0f193eee3..91459341cc 100644 --- a/examples/src/main/scala/sttp/tapir/examples/BooksExampleSemiauto.scala +++ b/examples/src/main/scala/sttp/tapir/examples/custom_types/BooksExampleSemiauto.scala @@ -1,4 +1,4 @@ -package sttp.tapir.examples +package sttp.tapir.examples.custom_types import com.typesafe.scalalogging.StrictLogging import sttp.tapir.Schema @@ -91,6 +91,7 @@ object BooksExampleSemiauto extends App with StrictLogging { import Endpoints._ import sttp.tapir.server.ServerEndpoint + import scala.concurrent.Future def booksServerEndpoints: List[ServerEndpoint[Any, Future]] = { @@ -138,12 +139,11 @@ object BooksExampleSemiauto extends App with StrictLogging { def startServer(serverEndpoints: List[ServerEndpoint[Any, Future]]): Unit = { import akka.actor.ActorSystem import akka.http.scaladsl.Http + import sttp.tapir.server.akkahttp.AkkaHttpServerInterpreter import scala.concurrent.Await import scala.concurrent.duration._ - import sttp.tapir.server.akkahttp.AkkaHttpServerInterpreter - implicit val actorSystem: ActorSystem = ActorSystem() import actorSystem.dispatcher val routes = AkkaHttpServerInterpreter().toRoute(serverEndpoints) diff --git a/examples/src/main/scala/sttp/tapir/examples/EndpointWithCustomTypes.scala b/examples/src/main/scala/sttp/tapir/examples/custom_types/EndpointWithCustomTypes.scala similarity index 97% rename from examples/src/main/scala/sttp/tapir/examples/EndpointWithCustomTypes.scala rename to examples/src/main/scala/sttp/tapir/examples/custom_types/EndpointWithCustomTypes.scala index 00522fa4b3..37c716f37a 100644 --- a/examples/src/main/scala/sttp/tapir/examples/EndpointWithCustomTypes.scala +++ b/examples/src/main/scala/sttp/tapir/examples/custom_types/EndpointWithCustomTypes.scala @@ -1,4 +1,4 @@ -package sttp.tapir.examples +package sttp.tapir.examples.custom_types import io.circe.{Decoder, Encoder} import sttp.tapir._ diff --git a/examples/src/main/scala/sttp/tapir/examples/CustomErrorsOnDecodeFailureAkkaServer.scala b/examples/src/main/scala/sttp/tapir/examples/errors/CustomErrorsOnDecodeFailureAkkaServer.scala similarity index 98% rename from examples/src/main/scala/sttp/tapir/examples/CustomErrorsOnDecodeFailureAkkaServer.scala rename to examples/src/main/scala/sttp/tapir/examples/errors/CustomErrorsOnDecodeFailureAkkaServer.scala index ce5472ef3a..87fa1027fe 100644 --- a/examples/src/main/scala/sttp/tapir/examples/CustomErrorsOnDecodeFailureAkkaServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/errors/CustomErrorsOnDecodeFailureAkkaServer.scala @@ -1,4 +1,4 @@ -package sttp.tapir.examples +package sttp.tapir.examples.errors import akka.actor.ActorSystem import akka.http.scaladsl.Http diff --git a/examples/src/main/scala/sttp/tapir/examples/ErrorOutputsAkkaServer.scala b/examples/src/main/scala/sttp/tapir/examples/errors/ErrorOutputsAkkaServer.scala similarity index 98% rename from examples/src/main/scala/sttp/tapir/examples/ErrorOutputsAkkaServer.scala rename to examples/src/main/scala/sttp/tapir/examples/errors/ErrorOutputsAkkaServer.scala index e1e0055f8f..070b2a26ef 100644 --- a/examples/src/main/scala/sttp/tapir/examples/ErrorOutputsAkkaServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/errors/ErrorOutputsAkkaServer.scala @@ -1,4 +1,4 @@ -package sttp.tapir.examples +package sttp.tapir.examples.errors import akka.actor.ActorSystem import akka.http.scaladsl.Http diff --git a/examples/src/main/scala/sttp/tapir/examples/MultipartFormUploadAkkaServer.scala b/examples/src/main/scala/sttp/tapir/examples/multipart/MultipartFormUploadAkkaServer.scala similarity index 98% rename from examples/src/main/scala/sttp/tapir/examples/MultipartFormUploadAkkaServer.scala rename to examples/src/main/scala/sttp/tapir/examples/multipart/MultipartFormUploadAkkaServer.scala index 0a1c6d2477..739aa3b603 100644 --- a/examples/src/main/scala/sttp/tapir/examples/MultipartFormUploadAkkaServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/multipart/MultipartFormUploadAkkaServer.scala @@ -1,4 +1,4 @@ -package sttp.tapir.examples +package sttp.tapir.examples.multipart import java.io.PrintWriter diff --git a/examples/src/main/scala/sttp/tapir/examples/PrometheusMetricsExample.scala b/examples/src/main/scala/sttp/tapir/examples/observability/PrometheusMetricsExample.scala similarity index 97% rename from examples/src/main/scala/sttp/tapir/examples/PrometheusMetricsExample.scala rename to examples/src/main/scala/sttp/tapir/examples/observability/PrometheusMetricsExample.scala index 2f6d2975e7..2aff73ee78 100644 --- a/examples/src/main/scala/sttp/tapir/examples/PrometheusMetricsExample.scala +++ b/examples/src/main/scala/sttp/tapir/examples/observability/PrometheusMetricsExample.scala @@ -1,4 +1,4 @@ -package sttp.tapir.examples +package sttp.tapir.examples.observability import akka.actor.ActorSystem import akka.http.scaladsl.Http diff --git a/examples/src/main/scala/sttp/tapir/examples/MultipleEndpointsDocumentationAkkaServer.scala b/examples/src/main/scala/sttp/tapir/examples/openapi/MultipleEndpointsDocumentationAkkaServer.scala similarity index 98% rename from examples/src/main/scala/sttp/tapir/examples/MultipleEndpointsDocumentationAkkaServer.scala rename to examples/src/main/scala/sttp/tapir/examples/openapi/MultipleEndpointsDocumentationAkkaServer.scala index f79c6b8944..4e0e36c2d3 100644 --- a/examples/src/main/scala/sttp/tapir/examples/MultipleEndpointsDocumentationAkkaServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/openapi/MultipleEndpointsDocumentationAkkaServer.scala @@ -1,4 +1,4 @@ -package sttp.tapir.examples +package sttp.tapir.examples.openapi import java.util.concurrent.atomic.AtomicReference import akka.actor.ActorSystem diff --git a/examples/src/main/scala/sttp/tapir/examples/MultipleEndpointsDocumentationHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/openapi/MultipleEndpointsDocumentationHttp4sServer.scala similarity index 98% rename from examples/src/main/scala/sttp/tapir/examples/MultipleEndpointsDocumentationHttp4sServer.scala rename to examples/src/main/scala/sttp/tapir/examples/openapi/MultipleEndpointsDocumentationHttp4sServer.scala index bfdc10321e..4c1808c6a8 100644 --- a/examples/src/main/scala/sttp/tapir/examples/MultipleEndpointsDocumentationHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/openapi/MultipleEndpointsDocumentationHttp4sServer.scala @@ -1,4 +1,4 @@ -package sttp.tapir.examples +package sttp.tapir.examples.openapi import cats.effect._ import cats.syntax.all._ diff --git a/examples/src/main/scala/sttp/tapir/examples/OpenapiExtensions.scala b/examples/src/main/scala/sttp/tapir/examples/openapi/OpenapiExtensions.scala similarity index 97% rename from examples/src/main/scala/sttp/tapir/examples/OpenapiExtensions.scala rename to examples/src/main/scala/sttp/tapir/examples/openapi/OpenapiExtensions.scala index ce2c335b91..377ce94a38 100644 --- a/examples/src/main/scala/sttp/tapir/examples/OpenapiExtensions.scala +++ b/examples/src/main/scala/sttp/tapir/examples/openapi/OpenapiExtensions.scala @@ -1,4 +1,4 @@ -package sttp.tapir.examples +package sttp.tapir.examples.openapi import io.circe.generic.auto._ import sttp.apispec.openapi.Info diff --git a/examples/src/main/scala/sttp/tapir/examples/RedocContextPathHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/openapi/RedocContextPathHttp4sServer.scala similarity index 97% rename from examples/src/main/scala/sttp/tapir/examples/RedocContextPathHttp4sServer.scala rename to examples/src/main/scala/sttp/tapir/examples/openapi/RedocContextPathHttp4sServer.scala index 6935ad8976..7f2615962b 100644 --- a/examples/src/main/scala/sttp/tapir/examples/RedocContextPathHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/openapi/RedocContextPathHttp4sServer.scala @@ -1,4 +1,4 @@ -package sttp.tapir.examples +package sttp.tapir.examples.openapi import cats.effect._ import cats.syntax.all._ diff --git a/examples/src/main/scala/sttp/tapir/examples/RedocZioHttpServer.scala b/examples/src/main/scala/sttp/tapir/examples/openapi/RedocZioHttpServer.scala similarity index 97% rename from examples/src/main/scala/sttp/tapir/examples/RedocZioHttpServer.scala rename to examples/src/main/scala/sttp/tapir/examples/openapi/RedocZioHttpServer.scala index fad9b96ca4..8de441aba3 100644 --- a/examples/src/main/scala/sttp/tapir/examples/RedocZioHttpServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/openapi/RedocZioHttpServer.scala @@ -1,4 +1,4 @@ -package sttp.tapir.examples +package sttp.tapir.examples.openapi import io.circe.generic.auto._ import sttp.tapir.generic.auto._ diff --git a/examples/src/main/scala/sttp/tapir/examples/SwaggerUIOauth2AkkaServer.scala b/examples/src/main/scala/sttp/tapir/examples/openapi/SwaggerUIOAuth2AkkaServer.scala similarity index 96% rename from examples/src/main/scala/sttp/tapir/examples/SwaggerUIOauth2AkkaServer.scala rename to examples/src/main/scala/sttp/tapir/examples/openapi/SwaggerUIOAuth2AkkaServer.scala index b2291d55cb..ef21d04a98 100644 --- a/examples/src/main/scala/sttp/tapir/examples/SwaggerUIOauth2AkkaServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/openapi/SwaggerUIOAuth2AkkaServer.scala @@ -1,4 +1,5 @@ -package sttp.tapir.examples +package sttp.tapir.examples.openapi + import akka.actor.ActorSystem import akka.http.scaladsl.Http import akka.http.scaladsl.server.{Route, RouteConcatenation} @@ -28,7 +29,7 @@ import scala.concurrent.{Await, Future, Promise} * * Go to: [[http://localhost:3333/docs]] And try authorize by using `Authorize` by providing details of clients and user */ -object SwaggerUIOauth2AkkaServer extends App with RouteConcatenation { +object SwaggerUIOAuth2AkkaServer extends App with RouteConcatenation { implicit val actorSystem: ActorSystem = ActorSystem() import actorSystem.dispatcher diff --git a/examples/src/main/scala/sttp/tapir/examples/BasicAuthenticationAkkaServer.scala b/examples/src/main/scala/sttp/tapir/examples/security/BasicAuthenticationAkkaServer.scala similarity index 98% rename from examples/src/main/scala/sttp/tapir/examples/BasicAuthenticationAkkaServer.scala rename to examples/src/main/scala/sttp/tapir/examples/security/BasicAuthenticationAkkaServer.scala index 1d8e7d6100..3dbf7685a7 100644 --- a/examples/src/main/scala/sttp/tapir/examples/BasicAuthenticationAkkaServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/security/BasicAuthenticationAkkaServer.scala @@ -1,4 +1,4 @@ -package sttp.tapir.examples +package sttp.tapir.examples.security import akka.actor.ActorSystem import akka.http.scaladsl.Http diff --git a/examples/src/main/scala/sttp/tapir/examples/OAuth2GithubHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/security/OAuth2GithubHttp4sServer.scala similarity index 99% rename from examples/src/main/scala/sttp/tapir/examples/OAuth2GithubHttp4sServer.scala rename to examples/src/main/scala/sttp/tapir/examples/security/OAuth2GithubHttp4sServer.scala index cebd8ff816..db589b099e 100644 --- a/examples/src/main/scala/sttp/tapir/examples/OAuth2GithubHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/security/OAuth2GithubHttp4sServer.scala @@ -1,4 +1,4 @@ -package sttp.tapir.examples +package sttp.tapir.examples.security import cats.effect._ import cats.syntax.all._ diff --git a/examples/src/main/scala/sttp/tapir/examples/ServerSecurityLogicAkka.scala b/examples/src/main/scala/sttp/tapir/examples/security/ServerSecurityLogicAkka.scala similarity index 98% rename from examples/src/main/scala/sttp/tapir/examples/ServerSecurityLogicAkka.scala rename to examples/src/main/scala/sttp/tapir/examples/security/ServerSecurityLogicAkka.scala index 72ce10aa09..f107f161c2 100644 --- a/examples/src/main/scala/sttp/tapir/examples/ServerSecurityLogicAkka.scala +++ b/examples/src/main/scala/sttp/tapir/examples/security/ServerSecurityLogicAkka.scala @@ -1,4 +1,4 @@ -package sttp.tapir.examples +package sttp.tapir.examples.security import akka.actor.ActorSystem import akka.http.scaladsl.Http diff --git a/examples/src/main/scala/sttp/tapir/examples/ServerSecurityLogicRefreshCookiesAkka.scala b/examples/src/main/scala/sttp/tapir/examples/security/ServerSecurityLogicRefreshCookiesAkka.scala similarity index 98% rename from examples/src/main/scala/sttp/tapir/examples/ServerSecurityLogicRefreshCookiesAkka.scala rename to examples/src/main/scala/sttp/tapir/examples/security/ServerSecurityLogicRefreshCookiesAkka.scala index e8eca1f7a1..2f87738e2b 100644 --- a/examples/src/main/scala/sttp/tapir/examples/ServerSecurityLogicRefreshCookiesAkka.scala +++ b/examples/src/main/scala/sttp/tapir/examples/security/ServerSecurityLogicRefreshCookiesAkka.scala @@ -1,4 +1,4 @@ -package sttp.tapir.examples +package sttp.tapir.examples.security import akka.actor.ActorSystem import akka.http.scaladsl.Http diff --git a/examples/src/main/scala/sttp/tapir/examples/StaticContentFromFilesAkkaServer.scala b/examples/src/main/scala/sttp/tapir/examples/static_content/StaticContentFromFilesAkkaServer.scala similarity index 97% rename from examples/src/main/scala/sttp/tapir/examples/StaticContentFromFilesAkkaServer.scala rename to examples/src/main/scala/sttp/tapir/examples/static_content/StaticContentFromFilesAkkaServer.scala index 21e613fae2..e9c888e241 100644 --- a/examples/src/main/scala/sttp/tapir/examples/StaticContentFromFilesAkkaServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/static_content/StaticContentFromFilesAkkaServer.scala @@ -1,4 +1,4 @@ -package sttp.tapir.examples +package sttp.tapir.examples.static_content import akka.actor.ActorSystem import akka.http.scaladsl.Http diff --git a/examples/src/main/scala/sttp/tapir/examples/StaticContentFromFilesNettyServer.scala b/examples/src/main/scala/sttp/tapir/examples/static_content/StaticContentFromFilesNettyServer.scala similarity index 90% rename from examples/src/main/scala/sttp/tapir/examples/StaticContentFromFilesNettyServer.scala rename to examples/src/main/scala/sttp/tapir/examples/static_content/StaticContentFromFilesNettyServer.scala index 385445e2db..e58f9b1943 100644 --- a/examples/src/main/scala/sttp/tapir/examples/StaticContentFromFilesNettyServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/static_content/StaticContentFromFilesNettyServer.scala @@ -1,4 +1,4 @@ -package sttp.tapir.examples +package sttp.tapir.examples.static_content import sttp.tapir.server.netty.NettyFutureServer import sttp.tapir.{emptyInput, filesServerEndpoints} diff --git a/examples/src/main/scala/sttp/tapir/examples/StaticContentFromResourcesAkkaServer.scala b/examples/src/main/scala/sttp/tapir/examples/static_content/StaticContentFromResourcesAkkaServer.scala similarity index 96% rename from examples/src/main/scala/sttp/tapir/examples/StaticContentFromResourcesAkkaServer.scala rename to examples/src/main/scala/sttp/tapir/examples/static_content/StaticContentFromResourcesAkkaServer.scala index 619fd6ddf0..83beadc796 100644 --- a/examples/src/main/scala/sttp/tapir/examples/StaticContentFromResourcesAkkaServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/static_content/StaticContentFromResourcesAkkaServer.scala @@ -1,4 +1,4 @@ -package sttp.tapir.examples +package sttp.tapir.examples.static_content import akka.actor.ActorSystem import akka.http.scaladsl.Http diff --git a/examples/src/main/scala/sttp/tapir/examples/StaticContentSecureAkkaServer.scala b/examples/src/main/scala/sttp/tapir/examples/static_content/StaticContentSecureAkkaServer.scala similarity index 97% rename from examples/src/main/scala/sttp/tapir/examples/StaticContentSecureAkkaServer.scala rename to examples/src/main/scala/sttp/tapir/examples/static_content/StaticContentSecureAkkaServer.scala index a869d6c964..0f442fb7f4 100644 --- a/examples/src/main/scala/sttp/tapir/examples/StaticContentSecureAkkaServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/static_content/StaticContentSecureAkkaServer.scala @@ -1,4 +1,4 @@ -package sttp.tapir.examples +package sttp.tapir.examples.static_content import akka.actor.ActorSystem import akka.http.scaladsl.Http diff --git a/examples/src/main/scala/sttp/tapir/examples/StreamingAkkaServer.scala b/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingAkkaServer.scala similarity index 97% rename from examples/src/main/scala/sttp/tapir/examples/StreamingAkkaServer.scala rename to examples/src/main/scala/sttp/tapir/examples/streaming/StreamingAkkaServer.scala index df63138578..561e88eb9f 100644 --- a/examples/src/main/scala/sttp/tapir/examples/StreamingAkkaServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingAkkaServer.scala @@ -1,4 +1,4 @@ -package sttp.tapir.examples +package sttp.tapir.examples.streaming import akka.actor.ActorSystem import akka.http.scaladsl.Http @@ -7,11 +7,11 @@ import akka.stream.scaladsl.Source import akka.util.ByteString import sttp.capabilities.akka.AkkaStreams import sttp.client3._ -import sttp.tapir._ import sttp.tapir.server.akkahttp.AkkaHttpServerInterpreter +import sttp.tapir._ -import scala.concurrent.duration._ import scala.concurrent.{Await, Future} +import scala.concurrent.duration._ object StreamingAkkaServer extends App { implicit val actorSystem: ActorSystem = ActorSystem() diff --git a/examples/src/main/scala/sttp/tapir/examples/StreamingHttp4sFs2Server.scala b/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingHttp4sFs2Server.scala similarity index 94% rename from examples/src/main/scala/sttp/tapir/examples/StreamingHttp4sFs2Server.scala rename to examples/src/main/scala/sttp/tapir/examples/streaming/StreamingHttp4sFs2Server.scala index 286f271f33..e6803a5151 100644 --- a/examples/src/main/scala/sttp/tapir/examples/StreamingHttp4sFs2Server.scala +++ b/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingHttp4sFs2Server.scala @@ -1,11 +1,11 @@ -package sttp.tapir.examples +package sttp.tapir.examples.streaming -import cats.effect._ -import cats.syntax.all._ -import fs2._ +import cats.effect.{ExitCode, IO, IOApp} +import cats.implicits._ +import fs2.{Chunk, Stream} import org.http4s.HttpRoutes -import org.http4s.server.Router import org.http4s.blaze.server.BlazeServerBuilder +import org.http4s.server.Router import sttp.capabilities.fs2.Fs2Streams import sttp.client3._ import sttp.model.HeaderNames diff --git a/examples/src/main/scala/sttp/tapir/examples/WebSocketAkkaClient.scala b/examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketAkkaClient.scala similarity index 98% rename from examples/src/main/scala/sttp/tapir/examples/WebSocketAkkaClient.scala rename to examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketAkkaClient.scala index a7ca304868..293f375f62 100644 --- a/examples/src/main/scala/sttp/tapir/examples/WebSocketAkkaClient.scala +++ b/examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketAkkaClient.scala @@ -1,4 +1,4 @@ -package sttp.tapir.examples +package sttp.tapir.examples.websocket import akka.actor.ActorSystem import akka.stream.scaladsl.{Flow, Sink, Source} @@ -16,7 +16,6 @@ import sttp.tapir.client.sttp.ws.akkahttp._ import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ import scala.concurrent.{Await, Future} - object WebSocketAkkaClient extends App { case class TestMessage(text: String, counter: Int) diff --git a/examples/src/main/scala/sttp/tapir/examples/WebSocketAkkaServer.scala b/examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketAkkaServer.scala similarity index 98% rename from examples/src/main/scala/sttp/tapir/examples/WebSocketAkkaServer.scala rename to examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketAkkaServer.scala index 8015178d8d..27fc1d683f 100644 --- a/examples/src/main/scala/sttp/tapir/examples/WebSocketAkkaServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketAkkaServer.scala @@ -1,4 +1,4 @@ -package sttp.tapir.examples +package sttp.tapir.examples.websocket import akka.actor.ActorSystem import akka.http.scaladsl.Http diff --git a/examples/src/main/scala/sttp/tapir/examples/WebSocketHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketHttp4sServer.scala similarity index 99% rename from examples/src/main/scala/sttp/tapir/examples/WebSocketHttp4sServer.scala rename to examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketHttp4sServer.scala index a9c4934dcb..1fff3bb737 100644 --- a/examples/src/main/scala/sttp/tapir/examples/WebSocketHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketHttp4sServer.scala @@ -1,4 +1,4 @@ -package sttp.tapir.examples +package sttp.tapir.examples.websocket import cats.effect.{ExitCode, IO, IOApp} import io.circe.generic.auto._ diff --git a/generated-doc/out/client/http4s.md b/generated-doc/out/client/http4s.md index fe957f6508..23cc75b914 100644 --- a/generated-doc/out/client/http4s.md +++ b/generated-doc/out/client/http4s.md @@ -3,7 +3,7 @@ Add the dependency: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-http4s-client" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-http4s-client" % "1.0.0-RC3" ``` To interpret an endpoint definition as an `org.http4s.Request[F]`, import: diff --git a/generated-doc/out/client/play.md b/generated-doc/out/client/play.md index 04134c46cf..3c3db869af 100644 --- a/generated-doc/out/client/play.md +++ b/generated-doc/out/client/play.md @@ -3,7 +3,7 @@ Add the dependency: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-play-client" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-play-client" % "1.0.0-RC3" ``` To make requests using an endpoint definition using the [play client](https://github.com/playframework/play-ws), import: diff --git a/generated-doc/out/client/sttp.md b/generated-doc/out/client/sttp.md index 31a22b52ad..8d7b8498cf 100644 --- a/generated-doc/out/client/sttp.md +++ b/generated-doc/out/client/sttp.md @@ -3,7 +3,7 @@ Add the dependency: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-sttp-client" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-sttp-client" % "1.0.0-RC3" ``` To make requests using an endpoint definition using the [sttp client](https://github.com/softwaremill/sttp), import: @@ -102,7 +102,7 @@ In this case add the following dependencies (note the [`%%%`](https://www.scala- instead of the usual `%%`): ```scala -"com.softwaremill.sttp.tapir" %%% "tapir-sttp-client" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %%% "tapir-sttp-client" % "1.0.0-RC3" "io.github.cquiroz" %%% "scala-java-time" % "2.2.0" // implementations of java.time classes for Scala.JS ``` diff --git a/generated-doc/out/conf.py b/generated-doc/out/conf.py index d10a79e679..56ae499234 100644 --- a/generated-doc/out/conf.py +++ b/generated-doc/out/conf.py @@ -61,9 +61,9 @@ # built documents. # # The short X.Y version. -version = u'0.x' +version = u'1.x' # The full version, including alpha/beta/rc tags. -release = u'0.x' +release = u'1.x' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/generated-doc/out/docs/asyncapi.md b/generated-doc/out/docs/asyncapi.md index 4bd369ae96..d0cd54892e 100644 --- a/generated-doc/out/docs/asyncapi.md +++ b/generated-doc/out/docs/asyncapi.md @@ -3,8 +3,8 @@ To use, add the following dependencies: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-asyncapi-docs" % "1.0.0-RC2" -"com.softwaremill.sttp.tapir" %% "tapir-asyncapi-circe-yaml" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-asyncapi-docs" % "1.0.0-RC3" +"com.softwaremill.sttp.apispec" %% "asyncapi-circe-yaml" % "..." // see https://github.com/softwaremill/sttp-apispec ``` Tapir contains a case class-based model of the asyncapi data structures in the `asyncapi/asyncapi-model` subproject (the diff --git a/generated-doc/out/docs/openapi.md b/generated-doc/out/docs/openapi.md index 6c11d9ca31..edb1dce0ba 100644 --- a/generated-doc/out/docs/openapi.md +++ b/generated-doc/out/docs/openapi.md @@ -1,5 +1,11 @@ # Generating OpenAPI documentation +To expose documentation, endpoints first need to be interpreted into an OpenAPI yaml or json. Then, the generated +description of our API can be exposed using a UI such as Swagger or Redoc. + +These two operations can be done in a single step, using the `SwaggerInterpreter` or `RedocInterpreter`. Or, if needed, +these steps can be done separately, giving you complete control over the process. + ## Generating and exposing documentation in a single step ### Using Swagger @@ -7,12 +13,13 @@ To generate OpenAPI documentation and expose it using the Swagger UI in a single step, first add the dependency: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-bundle" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-bundle" % "1.0.0-RC3" ``` -Then, you can interpret a list of endpoints, as server endpoints exposing the Swagger UI, using `SwaggerInterpreter`. -The result - a list of file-serving endpoints - will be configured to use the yaml corresponding to the passed -endpoints. The swagger endpoints will need in turn to be interpreted using your server interpreter. For example: +Then, you can interpret a list of endpoints using `SwaggerInterpreter`. The result will be a list of file-serving +server endpoints, which use the yaml corresponding to the endpoints passed originally. These swagger endpoints, together +with the endpoints for which the documentation is generated, will need in turn to be interpreted using your server +interpreter. For example: ```scala import sttp.tapir._ @@ -48,22 +55,22 @@ for details. Similarly as above, you'll need the following dependency: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-redoc-bundle" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-redoc-bundle" % "1.0.0-RC3" ``` And the server endpoints can be generated using the `sttp.tapir.redoc.bundle.RedocInterpreter` class. -## Generating OpenAPI documentation +## Generating OpenAPI documentation separately To generate the docs in the OpenAPI yaml format, add the following dependencies: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-openapi-docs" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-openapi-docs" % "1.0.0-RC3" "com.softwaremill.sttp.apispec" %% "openapi-circe-yaml" % "..." // see https://github.com/softwaremill/sttp-apispec ``` -Tapir contains a case class-based model of the openapi data structures in the `openapi/openapi-model` subproject (the -model is independent from all other tapir modules and can be used stand-alone). +The case-class based model of the openapi data structures is present in the [sttp-apispec](https://github.com/softwaremill/sttp-apispec) +project. An endpoint can be converted to an instance of the model by importing the `sttp.tapir.docs.openapi.OpenAPIDocsInterpreter` object: @@ -119,6 +126,43 @@ import sttp.apispec.openapi.circe._ println(Printer.spaces2.print(docs.asJson)) ``` +## Exposing generated OpenAPI documentation + +Exposing the OpenAPI can be done using [Swagger UI](https://swagger.io/tools/swagger-ui/) or +[Redoc](https://github.com/Redocly/redoc). You can either both interpret endpoints to OpenAPI's yaml and expose +them in a single step (see above), or you can do that separately. + +The modules `tapir-swagger-ui` and `tapir-redoc` contain server endpoint definitions, which given the documentation in +yaml format, will expose it using the given context path. To use, add as a dependency either +`tapir-swagger-ui`: +```scala +"com.softwaremill.sttp.tapir" %% "tapir-swagger-ui" % "1.0.0-RC3" +``` + +or `tapir-redoc`: +```scala +"com.softwaremill.sttp.tapir" %% "tapir-redoc" % "1.0.0-RC3" +``` + +Then, you'll need to pass the server endpoints to your server interpreter. For example, using akka-http: + +```scala +import sttp.apispec.openapi.circe.yaml._ +import sttp.tapir._ +import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter +import sttp.tapir.server.akkahttp.AkkaHttpServerInterpreter +import sttp.tapir.swagger.SwaggerUI + +import scala.concurrent.Future +import scala.concurrent.ExecutionContext.Implicits.global + +val myEndpoints: Seq[AnyEndpoint] = ??? +val docsAsYaml: String = OpenAPIDocsInterpreter().toOpenAPI(myEndpoints, "My App", "1.0").toYaml + +// add to your akka routes +val swaggerUIRoute = AkkaHttpServerInterpreter().toRoute(SwaggerUI[Future](docsAsYaml)) +``` + ## Options Options can be customised by providing an instance of `OpenAPIDocsOptions` to the interpreter: @@ -254,43 +298,6 @@ import sttp.tapir._ val acceptHeader: EndpointInput[String] = header[String]("Accept").schema(_.hidden(true)) ``` -## Exposing generated OpenAPI documentation - -Exposing the OpenAPI can be done using [Swagger UI](https://swagger.io/tools/swagger-ui/) or -[Redoc](https://github.com/Redocly/redoc). You can either both interpret endpoints to OpenAPI's yaml and expose -them in a single step (see above), or you can do that separately. - -The modules `tapir-swagger-ui` and `tapir-redoc` contain server endpoint definitions, which given the documentation in -yaml format, will expose it using the given context path. To use, add as a dependency either -`tapir-swagger-ui`: -```scala -"com.softwaremill.sttp.tapir" %% "tapir-swagger-ui" % "1.0.0-RC2" -``` - -or `tapir-redoc`: -```scala -"com.softwaremill.sttp.tapir" %% "tapir-redoc" % "1.0.0-RC2" -``` - -Then, you'll need to pass the server endpoints to your server interpreter. For example, using akka-http: - -```scala -import sttp.apispec.openapi.circe.yaml._ -import sttp.tapir._ -import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter -import sttp.tapir.server.akkahttp.AkkaHttpServerInterpreter -import sttp.tapir.swagger.SwaggerUI - -import scala.concurrent.Future -import scala.concurrent.ExecutionContext.Implicits.global - -val myEndpoints: Seq[AnyEndpoint] = ??? -val docsAsYaml: String = OpenAPIDocsInterpreter().toOpenAPI(myEndpoints, "My App", "1.0").toYaml - -// add to your akka routes -val swaggerUIRoute = AkkaHttpServerInterpreter().toRoute(SwaggerUI[Future](docsAsYaml)) -``` - ## Using SwaggerUI with sbt-assembly The `tapir-swagger-ui` and `tapir-swagger-ui-bundle` modules rely on a file in the `META-INF` directory tree, to diff --git a/generated-doc/out/endpoint/basics.md b/generated-doc/out/endpoint/basics.md index 8cbf2eb173..2494ba15c2 100644 --- a/generated-doc/out/endpoint/basics.md +++ b/generated-doc/out/endpoint/basics.md @@ -10,12 +10,11 @@ An endpoint is represented as a value of type `Endpoint[A, I, E, O, R]`, where: Input/output parameters (`A`, `I`, `E` and `O`) can be: -* of type `Unit`, when there's no input/ouput of the given type +* of type `Unit`, when there's no input/output * a single type * a tuple of types -Hence, an empty, initial endpoint (`tapir.endpoint`), with no inputs and no outputs, from which all other endpoints are -derived has the type: +Hence, an empty, initial endpoint, with no inputs and no outputs, from which all other endpoints are derived has the type: ```scala import sttp.tapir._ @@ -31,7 +30,7 @@ import sttp.tapir._ type PublicEndpoint[I, E, O, -R] = Endpoint[Unit, I, E, O, R] ``` -A public endpoint which accepts two parameters of types `UUID` and `Int`, upon error returns a `String`, and on normal +A public endpoint has two inputs of types `UUID` and `Int`, upon error returns a `String`, and on normal completion returns a `User`, would have the type: @@ -42,13 +41,16 @@ val userEndpoint: PublicEndpoint[(UUID, Int), String, User, Any] = ??? ``` You can think of an endpoint as a function, which takes input parameters of type `A` & `I` and returns a result of type -`Either[E, O]`, where inputs or outputs can contain streaming bodies of type `S`. +`Either[E, O]`. ### Infallible endpoints Note that the empty `endpoint` description maps no values to either error and success outputs, however errors -are still represented and allowed to occur. If you preferred to use an endpoint description, where -errors can not happen, use `infallibleEndpoint: PublicEndpoint[Unit, Nothing, Unit, Nothing]`. This might be useful when +are still represented and allowed to occur. In case of the error output, the single member of the unit type, `(): Unit`, +maps to an empty-body `400 Bad Request`. + +If you prefer to use an endpoint description, where errors cannot happen, use +`infallibleEndpoint: PublicEndpoint[Unit, Nothing, Unit, Any]`. This might be useful when interpreting endpoints [as a client](../client/sttp.md). ## Defining an endpoint diff --git a/generated-doc/out/endpoint/codecs.md b/generated-doc/out/endpoint/codecs.md index 82e61178cc..b4a9cd303a 100644 --- a/generated-doc/out/endpoint/codecs.md +++ b/generated-doc/out/endpoint/codecs.md @@ -4,20 +4,39 @@ A `Codec[L, H, CF]` is a bi-directional mapping between low-level values of type Low level values are formatted as `CF`. A codec also contains the schema of the high-level value, which is used for validation and documentation. -There are built-in codecs for most common types such as `String`, `Int` etc. Codecs are usually defined as implicit -values and resolved implicitly when they are referenced. However, they can also be provided explicitly as needed. +For example, a `Codec[String, User, CodecFormat.Json]` contains: +* a function to decode a `String` into `User`, which assumes that the string if formatted as JSON; this decoding might fail of course in case of malformed input +* a function to encode a `User` into a JSON `String`; this encoding step cannot fail -For example, a `query[Int]("quantity")` specifies an input parameter which corresponds to the `quantity` query +There are built-in implicit codecs for most common types such as `String`, `Int`, `Instant` etc., as well as some +types representing header values. Take a look at the `Codec` companion object for a full list. The companion object +also contains a number of helper methods to create custom codecs. + +## Looking up codecs + +For most inputs/outputs, the appropriate codec is required as an implicit parameter. Hence codec instances are usually +defined as implicit values and resolved implicitly when they are referenced. However, they can also be provided +explicitly as needed. + +As an example, a `query[Int]("quantity")` specifies an input parameter which corresponds to the `quantity` query parameter and will be mapped as an `Int`. A query input requires a codec, where the low-level value is a `List[String]` (representing potentially 0, one, or multiple parameters with the given name in the URL). Hence, an implicit `Codec[List[String], Int, TextPlain]` value will be looked up when using the `query` method (which is defined in the `sttp.tapir` package). In this example, the codec will verify that there's a single query parameter with the given name, and parse it as an -integer. If any of this fails, a failure will be reported. +integer. If any of this fails, a decode failure will be reported. + +However, in some cases codecs aren't looked up as implicit values, instead being created from simpler components, which +themselves are looked up as implicits. This is the case e.g. for json bodies specified using `jsonBody`. The rationale +behind such a design is that this provides better error reporting, in case the implicit components, used to create the +codec, are missing. Consult the signature of the specific input/output to learn what are its implicit requirements. + +## Decode failures In a server setting, if the value cannot be parsed as an int, a decoding failure is reported, and the endpoint -won't match the request, or a `400 Bad Request` response is returned (depending on configuration). +won't match the request, or a `400 Bad Request` response is returned (depending on configuration). Take a look at +[server error handling](../server/errors.md) for more details. ## Optional and multiple parameters diff --git a/generated-doc/out/endpoint/customtypes.md b/generated-doc/out/endpoint/customtypes.md index cdaaabf477..c8149d9cfd 100644 --- a/generated-doc/out/endpoint/customtypes.md +++ b/generated-doc/out/endpoint/customtypes.md @@ -1,8 +1,10 @@ # Custom types -To support a custom type, you'll need to provide an implicit `Codec` for that type. +To support a custom type, you'll need to provide an implicit `Codec` for that type, or the components to create such +a codec. The below mostly applies to wrapper types for inputs/outputs such as query parameters, path segments or +headers. For custom types used in [json](json.md) or [forms](forms.md) bodies, see the dedicated sections. -This can be done by writing a codec from scratch, mapping over an existing codec, or automatically deriving one. +A custom codec can be created by writing one from scratch, mapping over an existing codec, or automatically deriving one. Which of these approaches can be taken, depends on the context in which the codec will be used. ## Creating an implicit codec by hand diff --git a/generated-doc/out/endpoint/integrations.md b/generated-doc/out/endpoint/integrations.md index 0c86057c34..236f0915ac 100644 --- a/generated-doc/out/endpoint/integrations.md +++ b/generated-doc/out/endpoint/integrations.md @@ -1,12 +1,20 @@ # Datatypes integrations +```eval_rst +.. note:: + + Note that the codecs defined by the tapir integrations are used only when the specific types (e.g. enumerations0 are + used at the top level. Any nested usages (e.g. as part of a json body), need to be separately configured to work with + the used json library. +``` + ## Cats datatypes integration The `tapir-cats` module contains additional instances for some [cats](https://typelevel.org/cats/) datatypes as well as additional syntax: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-cats" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-cats" % "1.0.0-RC3" ``` - `import sttp.tapir.integ.cats.codec._` - brings schema, validator and codec instances @@ -18,7 +26,7 @@ If you use [refined](https://github.com/fthomas/refined), the `tapir-refined` mo validators for `T Refined P` as long as a codec for `T` already exists: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-refined" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-refined" % "1.0.0-RC3" ``` You'll need to extend the `sttp.tapir.codec.refined.TapirCodecRefined` @@ -39,7 +47,7 @@ The `tapir-enumeratum` module provides schemas, validators and codecs for [Enume enumerations. To use, add the following dependency: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-enumeratum" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-enumeratum" % "1.0.0-RC3" ``` Then, `import sttp.tapir.codec.enumeratum`, or extends the `sttp.tapir.codec.enumeratum.TapirCodecEnumeratum` trait. @@ -79,7 +87,7 @@ If you use [scala-newtype](https://github.com/estatico/scala-newtype), the `tapi schemas for a types with a `@newtype` and `@newsubtype` annotations as long as a codec and schema for its underlying value already exists: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-newtype" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-newtype" % "1.0.0-RC3" ``` Then, `import sttp.tapir.codec.newtype._`, or extend the `sttp.tapir.codec.enumeratum.TapirCodecNewType` trait to bring the implicit values into scope. @@ -91,7 +99,7 @@ For details refer to [derevo documentation](https://github.com/tofu-tf/derevo#in To use, add the following dependency: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-derevo" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-derevo" % "1.0.0-RC3" ``` Then you can derive schema for your ADT along with other typeclasses besides ADT declaration itself: diff --git a/generated-doc/out/endpoint/ios.md b/generated-doc/out/endpoint/ios.md index d716dd6379..d3ed2fef37 100644 --- a/generated-doc/out/endpoint/ios.md +++ b/generated-doc/out/endpoint/ios.md @@ -12,8 +12,9 @@ The `tapir` package contains a number of convenience methods to define an input For inputs, these are: * `path[T]`, which captures a path segment as an input parameter of type `T` -* any string, which will be implicitly converted to a fixed path segment. Path segments can be combined with the `/` - method, and don't map to any values (have type `EndpointInput[Unit]`) +* any string, which will be implicitly converted to a fixed path segment. Constant path segments can be combined with + the `/` method, and don't map to any values (they have type `EndpointInput[Unit]`, but they still modify the + endpoint's behavior) * `paths`, which maps to the whole remaining path as a `List[String]` * `query[T](name)` captures a query parameter with the given name * `queryParams` captures all query parameters, represented as `QueryParams` @@ -94,7 +95,7 @@ val statusEndpoint: PublicEndpoint[Unit, ErrorInfo, Status, Any] = baseEndpoint.in("status").out(jsonBody[Status]) ``` -The above endpoint will correspond to the `api/v1.0/status` path. +The above endpoint will correspond to the `/api/v1.0/status` path. ## Mapping over input/output values @@ -119,7 +120,7 @@ Next, you can use `mapDecode[II](f: I => DecodeResult[II])(g: II => I)`, to hand low-level value to a higher-value one) can fail. There's a couple of failure reasons, captured by the alternatives of the `DecodeResult` trait. -Mappings can also be done given an `Mapping[I, II]` instance. More on that in the secion on [codecs](codecs.md). +Mappings can also be done given a `Mapping[I, II]` instance. More on that in the section on [codecs](codecs.md). Creating a mapping between a tuple and a case class is a common operation, hence there's also a `mapTo[CaseClass]` method, which automatically provides the functions to construct/deconstruct the case class: @@ -226,7 +227,8 @@ To match a path prefix, first define inputs which match the path prefix, and the ### Arbitrary status codes To provide a (varying) status code of a server response, use the `statusCode` output, which maps to a value of type -`sttp.model.StatusCode`. The companion object contains known status codes as constants. This type of output is used only +`sttp.model.StatusCode`. In a server setting, the specific status code will then have to be provided dynamically by the +server logic. The companion object contains known status codes as constants. This type of output is used only when interpreting the endpoint as a server. If your endpoint returns varying status codes which you would like to have listed in documentation use `statusCode.description(code1, "code1 description").description(code2, "code2 description")` output. @@ -240,6 +242,12 @@ A fixed status code can be specified using the `statusCode(code)` output. Unless specified otherwise, successful responses are returned with the `200 OK` status code, and errors with `400 Bad Request`. For exception and decode failure handling, see [error handling](../server/errors.md). +### Different outputs with different status codes + +If you'd like to return different content together with a varying status code, use a [oneOf](oneof.md) output. +Each output variant can be paired with a fixed status code output (`statusCode(code)`), or a varying one, which will +be determined dynamically by the server logic. + ## Selected inputs/outputs for non-standard types * some header values can be decoded into a more structured representation, e.g. `header[MediaType]`, `header[ETag]`, diff --git a/generated-doc/out/endpoint/json.md b/generated-doc/out/endpoint/json.md index 6304da35c6..305b0f605e 100644 --- a/generated-doc/out/endpoint/json.md +++ b/generated-doc/out/endpoint/json.md @@ -1,12 +1,25 @@ # Working with JSON Json values are supported through codecs, which encode/decode values to json strings. Most often, you'll be using a -third-party library to perform the actual json parsing/printing. Currently, [zio-json](https://github.com/zio/zio-json), [Circe](https://github.com/circe/circe), [µPickle](http://www.lihaoyi.com/upickle/), [Spray JSON](https://github.com/spray/spray-json), [Play JSON](https://github.com/playframework/play-json), [Tethys JSON](https://github.com/tethys-json/tethys), [Jsoniter-scala](https://github.com/plokhotnyuk/jsoniter-scala), and [Json4s](https://github.com/json4s/json4s) are supported. +third-party library to perform the actual json parsing/printing. See below for the list of supported libraries. -All of the integrations, when imported into scope, define a `jsonBody[T]` method. This method depends on -library-specific implicits being in scope, and derives from them a json codec. The derivation also requires implicit -`Schema[T]` and `Validator[T]` instances, which should be automatically derived. For more details see documentation -on supporting [custom types](customtypes.md). +All the integrations, when imported into scope, define a `jsonBody[T]` method. + +Instead of providing the json codec as an implicit value, this method depends on library-specific implicits being in +scope, and basing on these values creates a json codec. The derivation also requires +an implicit `Schema[T]` instance, which can be automatically derived. For more details see sections on +[schema derivation](schemas.md) and on supporting [custom types](customtypes.md) in general. Such a design provides +better error reporting, in case one of the components required to create the json codec is missing. + +```eval_rst +.. note:: + + Note that the process of deriving schemas, and deriving library-specific json encoders and decoders is entirely + separate. The first is controlled by tapir, the second - by the json library. Any customisation, e.g. for field + naming or inheritance strategies, must be done separately for both derivations. +``` + +## Implicit json codecs If you have a custom, implicit `Codec[String, T, Json]` instance, you should use the `customJsonBody[T]` method instead. This description of endpoint input/output, instead of deriving a codec basing on other library-specific implicits, uses @@ -14,10 +27,10 @@ the json codec that is in scope. ## Circe -To use Circe, add the following dependency to your project: +To use [Circe](https://github.com/circe/circe), add the following dependency to your project: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-json-circe" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-json-circe" % "1.0.0-RC3" ``` Next, import the package (or extend the `TapirJsonCirce` trait, see [MyTapir](../mytapir.md)): @@ -26,15 +39,12 @@ Next, import the package (or extend the `TapirJsonCirce` trait, see [MyTapir](.. import sttp.tapir.json.circe._ ``` -This will allow automatically deriving `Codec`s which, given an in-scope circe `Encoder`/`Decoder` and a `Schema`, -will create a codec using the json media type. Circe includes a couple of approaches to generating encoders/decoders +The above import brings into scope the `jsonBody[T]` body input/output description, which creates a codec, given an +in-scope circe `Encoder`/`Decoder` and a `Schema`. Circe includes a couple of approaches to generating encoders/decoders (manual, semi-auto and auto), so you may choose whatever suits you. Note that when using Circe's auto derivation, any encoders/decoders for custom types must be in scope as well. -Additionally, the above import brings into scope the `jsonBody[T]` body input/output description, which uses the above -codec. - For example, to automatically generate a JSON codec for a case class: ```scala @@ -48,6 +58,8 @@ case class Book(author: String, title: String, year: Int) val bookInput: EndpointIO[Book] = jsonBody[Book] ``` +### Configuring the circe printer + Circe lets you select an instance of `io.circe.Printer` to configure the way JSON objects are rendered. By default Tapir uses `Printer.nospaces`, which would render: @@ -88,10 +100,10 @@ Now the above JSON object will render as ## µPickle -To use µPickle add the following dependency to your project: +To use [µPickle](http://www.lihaoyi.com/upickle/) add the following dependency to your project: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-json-upickle" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-json-upickle" % "1.0.0-RC3" ``` Next, import the package (or extend the `TapirJsonuPickle` trait, see [MyTapir](../mytapir.md) and add `TapirJsonuPickle` not `TapirCirceJson`): @@ -100,7 +112,7 @@ Next, import the package (or extend the `TapirJsonuPickle` trait, see [MyTapir]( import sttp.tapir.json.upickle._ ``` -µPickle requires a ReadWriter in scope for each type you want to serialize. In order to provide one use the `macroRW` macro in the companion object as follows: +µPickle requires a `ReadWriter` in scope for each type you want to serialize. In order to provide one use the `macroRW` macro in the companion object as follows: ```scala import sttp.tapir._ @@ -123,10 +135,10 @@ For more examples, including making a custom encoder/decoder, see [TapirJsonuPic ## Play JSON -To use Play JSON add the following dependency to your project: +To use [Play JSON](https://github.com/playframework/play-json) add the following dependency to your project: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-json-play" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-json-play" % "1.0.0-RC3" ``` Next, import the package (or extend the `TapirJsonPlay` trait, see [MyTapir](../mytapir.md) and add `TapirJsonPlay` not `TapirCirceJson`): @@ -139,10 +151,10 @@ Play JSON requires `Reads` and `Writes` implicit values in scope for each type y ## Spray JSON -To use Spray JSON add the following dependency to your project: +To use [Spray JSON](https://github.com/spray/spray-json) add the following dependency to your project: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-json-spray" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-json-spray" % "1.0.0-RC3" ``` Next, import the package (or extend the `TapirJsonSpray` trait, see [MyTapir](../mytapir.md) and add `TapirJsonSpray` not `TapirCirceJson`): @@ -155,10 +167,10 @@ Spray JSON requires a `JsonFormat` implicit value in scope for each type you wan ## Tethys JSON -To use Tethys JSON add the following dependency to your project: +To use [Tethys JSON](https://github.com/tethys-json/tethys) add the following dependency to your project: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-json-tethys" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-json-tethys" % "1.0.0-RC3" ``` Next, import the package (or extend the `TapirJsonTethys` trait, see [MyTapir](../mytapir.md) and add `TapirJsonTethys` not `TapirCirceJson`): @@ -174,7 +186,7 @@ Tethys JSON requires `JsonReader` and `JsonWriter` implicit values in scope for To use [Jsoniter-scala](https://github.com/plokhotnyuk/jsoniter-scala) add the following dependency to your project: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-jsoniter-scala" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-jsoniter-scala" % "1.0.0-RC3" ``` Next, import the package (or extend the `TapirJsonJsoniter` trait, see [MyTapir](../mytapir.md) and add `TapirJsonJsoniter` not `TapirCirceJson`): @@ -185,13 +197,12 @@ import sttp.tapir.json.jsoniter._ Jsoniter Scala requires `JsonValueCodec` implicit value in scope for each type you want to serialize. - ## Json4s To use [json4s](https://github.com/json4s/json4s) add the following dependencies to your project: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-json-json4s" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-json-json4s" % "1.0.0-RC3" ``` And one of the implementations: @@ -219,10 +230,10 @@ implicit val formats: Formats = org.json4s.jackson.Serialization.formats(NoTypeH ## Zio JSON -To use Zio JSON, add the following dependency to your project: +To use [zio-json](https://github.com/zio/zio-json), add the following dependency to your project: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-json-zio" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-json-zio" % "1.0.0-RC3" ``` Next, import the package (or extend the `TapirJsonZio` trait, see [MyTapir](../mytapir.md) and add `TapirJsonZio` instead of `TapirCirceJson`): @@ -238,14 +249,6 @@ To add support for additional JSON libraries, see the [sources](https://github.com/softwaremill/tapir/blob/master/json/circe/src/main/scala/sttp/tapir/json/circe/TapirJsonCirce.scala) for the Circe codec (which is just a couple of lines of code). -## Schemas - -To derive json codecs automatically, not only implicits from the base library are needed (e.g. a circe -`Encoder`/`Decoder`), but also an implicit `Schema[T]` value, which provides a mapping between a type `T` and its -schema. A schema-for value contains a single `schema: Schema` field. - -See [custom types](customtypes.md) for details. - ## Next Read on about [working with forms](forms.md). diff --git a/generated-doc/out/endpoint/schemas.md b/generated-doc/out/endpoint/schemas.md index 6ccd5dc1b6..5b3fee8d01 100644 --- a/generated-doc/out/endpoint/schemas.md +++ b/generated-doc/out/endpoint/schemas.md @@ -1,5 +1,7 @@ # Schema derivation +A schema describes the shape of a value, how the low-level representation should be structured. + Implicit schemas for basic types (`String`, `Int`, etc.), and their collections (`Option`, `List`, `Array` etc.) are defined out-of-the box. They don't contain any meta-data, such as descriptions or example values. @@ -65,7 +67,6 @@ values must be `lazy val`s. even when otherwise auto derivation is used. ``` - ## Derivation for recursive types in Scala3 In Scala3, any schemas for recursive types need to be provided as typed `implicit def` (not a `given`)! diff --git a/generated-doc/out/endpoint/validation.md b/generated-doc/out/endpoint/validation.md index a0a5a76907..f309b34e94 100644 --- a/generated-doc/out/endpoint/validation.md +++ b/generated-doc/out/endpoint/validation.md @@ -2,7 +2,11 @@ Tapir supports validation for primitive types. Validation of composite values, whole data structures, business rules enforcement etc. should be done as part of the [server logic](../server/logic.md) of the endpoint, using the -dedicated error output (the `E` in `Endpoint[I, E, O, S]`) to report errors. +dedicated error output (the `E` in `Endpoint[A, I, E, O, S]`) to report errors. + +For some guidelines as to where to perform a specific type of validation, see the ["Validation analysis paralysis"](https://blog.softwaremill.com/validation-analysis-paralysis-ca9bdef0a6d7) article. A good indicator as to where to place particular validation +logic might be if the property that we are checking is a format error, or a business-level error? The validation +capabilities described in this section are intended only for format errors. ## Single type validation diff --git a/generated-doc/out/examples.md b/generated-doc/out/examples.md index 9bfa044df4..5087883fd6 100644 --- a/generated-doc/out/examples.md +++ b/generated-doc/out/examples.md @@ -1,8 +1,8 @@ # 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/examples/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 [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. -## Other examples +## Third-party examples To see an example project using tapir, [check out this Todo-Backend](https://github.com/hejfelix/tapir-http4s-todo-mvc) using tapir and http4s. @@ -12,12 +12,16 @@ A new project can be created using: `sbt new https://codeberg.org/wegtam/http4s- ## Blogs, articles +* [Security improvements in tapir 0.19](https://softwaremill.com/security-improvements-in-tapir-0-19/) +* [Tapir serverless: a proof of concept](https://blog.softwaremill.com/tapir-serverless-a-proof-of-concept-6b8c9de4d396) * [Designing tapir's WebSockets support](https://blog.softwaremill.com/designing-tapirs-websockets-support-ff1573166368) * [Three easy endpoints](https://blog.softwaremill.com/three-easy-endpoints-a6cbd52b0a6e) -* [tAPIr’s Endpoint meets ZIO’s IO](https://blog.softwaremill.com/tapirs-endpoint-meets-zio-s-io-3278099c5e10) +* [tAPIr's Endpoint meets ZIO's IO](https://blog.softwaremill.com/tapirs-endpoint-meets-zio-s-io-3278099c5e10) * [Describe, then interpret: HTTP endpoints using tapir](https://blog.softwaremill.com/describe-then-interpret-http-endpoints-using-tapir-ac139ba565b0) * [Functional pancakes](https://blog.softwaremill.com/functional-pancakes-cf70023f0dcb) ## Videos +* [ScalaLove 2020: Your HTTP endpoints are data, as well!](https://www.youtube.com/watch?v=yuQNgZgSFIc&t=944s) +* [Scalar 2020: A Functional Scala Stack For 2020](https://www.youtube.com/watch?v=DGlkap5kzGU) * [ScalaWorld 2019: Designing Programmer-Friendly APIs](https://www.youtube.com/watch?v=I3loMuHnYqw) diff --git a/generated-doc/out/generator/sbt-openapi-codegen.md b/generated-doc/out/generator/sbt-openapi-codegen.md index 7dc20f7cb3..30b44caaba 100644 --- a/generated-doc/out/generator/sbt-openapi-codegen.md +++ b/generated-doc/out/generator/sbt-openapi-codegen.md @@ -11,7 +11,7 @@ Add the sbt plugin to the `project/plugins.sbt`: ```scala -addSbtPlugin("com.softwaremill.sttp.tapir" % "sbt-openapi-codegen" % "1.0.0-RC2") +addSbtPlugin("com.softwaremill.sttp.tapir" % "sbt-openapi-codegen" % "1.0.0-RC3") ``` Enable the plugin for your project in the `build.sbt`: diff --git a/generated-doc/out/index.md b/generated-doc/out/index.md index cb363854bc..528c1bfd8d 100644 --- a/generated-doc/out/index.md +++ b/generated-doc/out/index.md @@ -1,13 +1,6 @@ -# tapir, or Typed API descRiptions +# tapir -## Why tapir? - -* **type-safety**: compile-time guarantees, develop-time completions, read-time information -* **declarative**: separate the shape of the endpoint (the "what"), from the server logic (the "how") -* **OpenAPI / Swagger integration**: generate documentation from endpoint descriptions -* **observability**: leverage the metadata to report rich metrics and tracing information -* **abstraction**: re-use common endpoint definitions, as well as individual inputs/outputs -* **library, not a framework**: integrates with your stack +Declarative, type-safe web endpoints library. ## Intro @@ -34,16 +27,28 @@ input and output parameters. An endpoint specification can be interpreted as: * [OpenAPI](docs/openapi.md) * [AsyncAPI](docs/asyncapi.md) -Tapir is licensed under Apache2, the source code is [available on GitHub](https://github.com/softwaremill/tapir). - Depending on how you prefer to explore the library, take a look at one of the [examples](examples.md) or read on for a more detailed description of how tapir works! +## Why tapir? + +* **type-safety**: compile-time guarantees, develop-time completions, read-time information +* **declarative**: separate the shape of the endpoint (the "what"), from the server logic (the "how") +* **OpenAPI / Swagger integration**: generate documentation from endpoint descriptions +* **observability**: leverage the metadata to report rich metrics and tracing information +* **abstraction**: re-use common endpoint definitions, as well as individual inputs/outputs +* **library, not a framework**: integrates with your stack + +## Availability + Tapir is available: * all modules - Scala 2.12 and 2.13 on the JVM -* selected modules (core; http4s, vertx, netty, aws servers; sttp and http4s clients; openapi; some js and datatype integrations) - Scala 3 on the JVM -* selected modules (aws server; sttp client; some js and datatype integrations) - Scala 2.12, 2.13 and 3 using Scala.JS. +* selected modules - Scala 3 on the JVM +* selected modules - Scala 2.12, 2.13 and 3 using Scala.JS +* selected modules - Scala 2.12, 2.13 and 3 using Scala Native + +Tapir is licensed under Apache2, the source code is [available on GitHub](https://github.com/softwaremill/tapir). ## Adopters @@ -51,7 +56,7 @@ Is your company already using tapir? We're continually expanding the "adopters" Please email us at [tapir@softwaremill.com](mailto:tapir@softwaremill.com) from your company's email with a link to your logo (if we can use it, of course!) or with details who to kindly ask for permission to feature the logo in tapir's documentation. We'll handle the rest. -We're seeing tapir's download numbers going steadily up; as we're nearing 1.0, the additional confidence boost for newcomers will help us to build tapir's ecosystem and make it thrive. Thank you! :) +Thank you!
Adobe @@ -84,16 +89,16 @@ import io.circe.generic.auto._ type Limit = Int type AuthToken = String -case class BooksFromYear(genre: String, year: Int) +case class BooksQuery(genre: String, year: Int) case class Book(title: String) // Define an endpoint -val booksListing: PublicEndpoint[(BooksFromYear, Limit, AuthToken), String, List[Book], Any] = +val booksListing: PublicEndpoint[(BooksQuery, Limit, AuthToken), String, List[Book], Any] = endpoint .get - .in(("books" / path[String]("genre") / path[Int]("year")).mapTo[BooksFromYear]) + .in(("books" / path[String]("genre") / path[Int]("year")).mapTo[BooksQuery]) .in(query[Limit]("limit").description("Maximum number of books to retrieve")) .in(header[AuthToken]("X-Auth-Token")) .errorOut(stringBody) @@ -116,7 +121,7 @@ import akka.http.scaladsl.server.Route import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits.global -def bookListingLogic(bfy: BooksFromYear, +def bookListingLogic(bfy: BooksQuery, limit: Limit, at: AuthToken): Future[Either[String, List[Book]]] = Future.successful(Right(List(Book("The Sorrows of Young Werther")))) @@ -133,7 +138,7 @@ import sttp.client3._ val booksListingRequest: Request[DecodeResult[Either[String, List[Book]]], Any] = SttpClientInterpreter() .toRequest(booksListing, Some(uri"http://localhost:8080")) - .apply((BooksFromYear("SF", 2016), 20, "xyz-abc-123")) + .apply((BooksQuery("SF", 2016), 20, "xyz-abc-123")) ``` ## Other sttp projects @@ -143,6 +148,8 @@ sttp is a family of Scala HTTP-related projects, and currently includes: * [sttp client](https://github.com/softwaremill/sttp): the Scala HTTP client you always wanted! * sttp tapir: this project * [sttp model](https://github.com/softwaremill/sttp-model): simple HTTP model classes (used by client & tapir) +* [sttp shared](https://github.com/softwaremill/sttp-shared): shared web socket, FP abstractions, capabilities and streaming code. +* [sttp apispec](https://github.com/softwaremill/sttp-apispec): OpenAPI, AsyncAPI and JSON Schema models. ## Sponsors @@ -150,7 +157,7 @@ Development and maintenance of sttp tapir is sponsored by [SoftwareMill](https:/ [![](https://files.softwaremill.com/logo/logo.png "SoftwareMill")](https://softwaremill.com) -# Table of contents +## Table of contents ```eval_rst .. toctree:: @@ -159,7 +166,7 @@ Development and maintenance of sttp tapir is sponsored by [SoftwareMill](https:/ quickstart examples - goals + stability .. toctree:: :maxdepth: 2 @@ -237,5 +244,6 @@ Development and maintenance of sttp tapir is sponsored by [SoftwareMill](https:/ mytapir troubleshooting migrating + goals contributing diff --git a/generated-doc/out/quickstart.md b/generated-doc/out/quickstart.md index b0727743c3..55fac966f5 100644 --- a/generated-doc/out/quickstart.md +++ b/generated-doc/out/quickstart.md @@ -3,7 +3,7 @@ To use tapir, add the following dependency to your project: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-core" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-core" % "1.0.0-RC3" ``` This will import only the core classes needed to create endpoint descriptions. To generate a server or a client, you @@ -16,14 +16,6 @@ you import the main package entirely, i.e.: import sttp.tapir._ ``` -Partial unification is now enabled by default from Scala 2.13. However, if you're using Scala 2.12 or older, and don't -have it already, you'll want to to enable partial unification in the compiler (alternatively, you'll need to manually -provide type arguments in some cases). In sbt, this is: - -```scala -scalacOptions += "-Ypartial-unification" -``` - Finally, type: ```scala @@ -31,3 +23,13 @@ endpoint. ``` and see where auto-complete gets you! + +## Scala 2.12 + +Partial unification is now enabled by default from Scala 2.13. However, if you're using Scala 2.12 or older, and don't +have it already, you'll want to to enable partial unification in the compiler (alternatively, you'll need to manually +provide type arguments in some cases). In sbt, this is: + +```scala +scalacOptions += "-Ypartial-unification" +``` \ No newline at end of file diff --git a/generated-doc/out/server/akkahttp.md b/generated-doc/out/server/akkahttp.md index bc8dacc08a..5628265648 100644 --- a/generated-doc/out/server/akkahttp.md +++ b/generated-doc/out/server/akkahttp.md @@ -4,14 +4,14 @@ To expose an endpoint as an [akka-http](https://doc.akka.io/docs/akka-http/curre dependency: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-akka-http-server" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-akka-http-server" % "1.0.0-RC3" ``` This will transitively pull some Akka modules in version 2.6. If you want to force your own Akka version (for example 2.5), use sbt exclusion. Mind the Scala version in artifact name: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-akka-http-server" % "1.0.0-RC2" exclude("com.typesafe.akka", "akka-stream_2.12") +"com.softwaremill.sttp.tapir" %% "tapir-akka-http-server" % "1.0.0-RC3" exclude("com.typesafe.akka", "akka-stream_2.12") ``` Now import the object: diff --git a/generated-doc/out/server/armeria.md b/generated-doc/out/server/armeria.md index 857ae0aade..e6759d4ff3 100644 --- a/generated-doc/out/server/armeria.md +++ b/generated-doc/out/server/armeria.md @@ -8,7 +8,7 @@ Armeria interpreter can be used with different effect systems (cats-effect, ZIO) Add the following dependency ```scala -"com.softwaremill.sttp.tapir" %% "tapir-armeria-server" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-armeria-server" % "1.0.0-RC3" ``` and import the object: @@ -75,7 +75,7 @@ Note that Armeria automatically injects an `ExecutionContext` on top of Armeria' Add the following dependency ```scala -"com.softwaremill.sttp.tapir" %% "tapir-armeria-server-cats" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-armeria-server-cats" % "1.0.0-RC3" ``` to use this interpreter with Cats Effect typeclasses. @@ -155,9 +155,9 @@ Add the following dependency ```scala // for zio 2: -"com.softwaremill.sttp.tapir" %% "tapir-armeria-server-zio" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-armeria-server-zio" % "1.0.0-RC3" // for zio 1: -"com.softwaremill.sttp.tapir" %% "tapir-armeria-server-zio1" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-armeria-server-zio1" % "1.0.0-RC3" ``` to use this interpreter with ZIO. diff --git a/generated-doc/out/server/aws.md b/generated-doc/out/server/aws.md index 93bc9bbb3d..b75e101c12 100644 --- a/generated-doc/out/server/aws.md +++ b/generated-doc/out/server/aws.md @@ -13,7 +13,7 @@ To implement the Lambda function, a server interpreter is available, which takes Currently, only an interpreter integrating with cats-effect is available (`AwsCatsEffectServerInterpreter`). To use, add the following dependency: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-aws-lambda" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-aws-lambda" % "1.0.0-RC3" ``` To configure API Gateway and the Lambda function, you can use: @@ -24,8 +24,8 @@ To configure API Gateway and the Lambda function, you can use: Add one of the following dependencies: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-aws-sam" % "1.0.0-RC2" -"com.softwaremill.sttp.tapir" %% "tapir-aws-terraform" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-aws-sam" % "1.0.0-RC3" +"com.softwaremill.sttp.tapir" %% "tapir-aws-terraform" % "1.0.0-RC3" ``` ## Examples diff --git a/generated-doc/out/server/finatra.md b/generated-doc/out/server/finatra.md index df6c486704..82678ac036 100644 --- a/generated-doc/out/server/finatra.md +++ b/generated-doc/out/server/finatra.md @@ -4,7 +4,7 @@ To expose an endpoint as an [finatra](https://twitter.github.io/finatra/) server dependency: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-finatra-server" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-finatra-server" % "1.0.0-RC3" ``` and import the object: @@ -17,7 +17,7 @@ This interpreter supports the twitter `Future`. Or, if you would like to use cats-effect project, you can add the following dependency: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-finatra-server-cats" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-finatra-server-cats" % "1.0.0-RC3" ``` and import the object: diff --git a/generated-doc/out/server/http4s.md b/generated-doc/out/server/http4s.md index 5646230bbb..1700b76118 100644 --- a/generated-doc/out/server/http4s.md +++ b/generated-doc/out/server/http4s.md @@ -4,7 +4,7 @@ To expose an endpoint as an [http4s](https://http4s.org) server, first add the f dependency: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-http4s-server" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-http4s-server" % "1.0.0-RC3" ``` and import the object: diff --git a/generated-doc/out/server/interceptors.md b/generated-doc/out/server/interceptors.md index a8054440a9..7fb523bedb 100644 --- a/generated-doc/out/server/interceptors.md +++ b/generated-doc/out/server/interceptors.md @@ -29,6 +29,7 @@ The following interceptors are used by default, and if enabled, called in this o * exception interceptor * logging interceptor +* unsupported media type interceptor * decode failure handler interceptor Note that while the request will be passed top-to-bottom, handling of the result will be done in opposite order. @@ -38,4 +39,11 @@ only later passed to the exception interceptor. Using `customiseInterceptors` on the options companion object, it is possible to customise the built-in interceptors. New ones can be prepended to the interceptor stack using `.prependInterceptor`, added before the decode failure interceptor using `.addInterceptor`, or appended using `.appendInterceptor`. Customisation can include removing the interceptor -altogether. \ No newline at end of file +altogether. + +## Attributes + +When implementing interceptors, it might be useful to take advantage of attributes, which can be attached both to +requests, as well as endpoint descriptions. Attributes are keyed using an `AttributeKey`. Typically, each attribute +corresponds to a unique type, and the key instance for that type can be created using `AttributeKey[T]`. The attribute +values then have to be of the given type `T`. \ No newline at end of file diff --git a/generated-doc/out/server/netty.md b/generated-doc/out/server/netty.md index ea3607a5af..7ecdd8ec21 100644 --- a/generated-doc/out/server/netty.md +++ b/generated-doc/out/server/netty.md @@ -5,7 +5,7 @@ To expose an endpoint using a [Netty](https://netty.io)-based server, first add the following dependency: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-netty-server" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-netty-server" % "1.0.0-RC3" ``` Then, use: @@ -75,5 +75,5 @@ can be passed using the `NettyFutureServer(options)` methods. Options may also b Add the following dependency: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-netty-server-cats" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-netty-server-cats" % "1.0.0-RC3" ``` \ No newline at end of file diff --git a/generated-doc/out/server/observability.md b/generated-doc/out/server/observability.md index 8312b8544b..b62d938a31 100644 --- a/generated-doc/out/server/observability.md +++ b/generated-doc/out/server/observability.md @@ -49,7 +49,7 @@ val labels = MetricLabels( Add the following dependency: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-prometheus-metrics" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-prometheus-metrics" % "1.0.0-RC3" ``` `PrometheusMetrics` encapsulates `CollectorReqistry` and `Metric` instances. It provides several ready to use metrics as @@ -130,7 +130,7 @@ val prometheusMetrics = PrometheusMetrics[Future]("tapir", CollectorRegistry.def Add the following dependency: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-opentelemetry-metrics" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-opentelemetry-metrics" % "1.0.0-RC3" ``` OpenTelemetry metrics are vendor-agnostic and can be exported using one diff --git a/generated-doc/out/server/play.md b/generated-doc/out/server/play.md index 4af038feaf..4fed3d4a59 100644 --- a/generated-doc/out/server/play.md +++ b/generated-doc/out/server/play.md @@ -3,7 +3,7 @@ To expose endpoint as a [play-server](https://www.playframework.com/) first add the following dependencies: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-play-server" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-play-server" % "1.0.0-RC3" ``` and (if you don't already depend on Play) diff --git a/generated-doc/out/server/vertx.md b/generated-doc/out/server/vertx.md index c4cd413f3a..e0cf2046a7 100644 --- a/generated-doc/out/server/vertx.md +++ b/generated-doc/out/server/vertx.md @@ -8,7 +8,7 @@ Vert.x interpreter can be used with different effect systems (cats-effect, ZIO) Add the following dependency ```scala -"com.softwaremill.sttp.tapir" %% "tapir-vertx-server" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-vertx-server" % "1.0.0-RC3" ``` to use this interpreter with `Future`. @@ -63,7 +63,7 @@ It's also possible to define an endpoint together with the server logic in a sin Add the following dependency ```scala -"com.softwaremill.sttp.tapir" %% "tapir-vertx-server-cats" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-vertx-server-cats" % "1.0.0-RC3" ``` to use this interpreter with Cats Effect typeclasses. @@ -146,9 +146,9 @@ Add the following dependency ```scala // for zio2: -"com.softwaremill.sttp.tapir" %% "tapir-vertx-server-zio" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-vertx-server-zio" % "1.0.0-RC3" // for zio1: -"com.softwaremill.sttp.tapir" %% "tapir-vertx-server-zio1" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-vertx-server-zio1" % "1.0.0-RC3" ``` to use this interpreter with ZIO. @@ -180,7 +180,7 @@ object Short extends ZIOAppDefault { .in(query[String]("key")) .out(plainBody[String]) - val attach = VertxZioServerInterpreter().route(responseEndpoint.zServerLogic { key => UIO.succeed(key) }) + val attach = VertxZioServerInterpreter().route(responseEndpoint.zServerLogic { key => ZIO.succeed(key) }) override def run = { ZIO.scoped( diff --git a/generated-doc/out/server/zio-http4s.md b/generated-doc/out/server/zio-http4s.md index 143c2e5b61..2a4c5394e4 100644 --- a/generated-doc/out/server/zio-http4s.md +++ b/generated-doc/out/server/zio-http4s.md @@ -9,16 +9,16 @@ The `*-zio` modules depend on ZIO 2.x. For ZIO 1.x support, use modules with the You'll need the following dependency for the `ZServerEndpoint` type alias and helper classes: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-zio" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-zio" % "1.0.0-RC3" ``` or just add the zio-http4s integration which already depends on `tapir-zio`: ```scala // for zio 2: -"com.softwaremill.sttp.tapir" %% "tapir-http4s-server-zio" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-http4s-server-zio" % "1.0.0-RC3" // for zio 1: -"com.softwaremill.sttp.tapir" %% "tapir-http4s-server-zio1" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-http4s-server-zio1" % "1.0.0-RC3" ``` Next, instead of the usual `import sttp.tapir._`, you should import (or extend the `ZTapir` trait, see [MyTapir](../mytapir.md)): @@ -142,7 +142,7 @@ val wsRoutes: WebSocketBuilder2[Task] => HttpRoutes[Task] = val serve: Task[Unit] = BlazeServerBuilder[Task] - .withExecutionContext(runtime.runtimeConfig.executor.asExecutionContext) + .withExecutionContext(runtime.executor.asExecutionContext) .bindHttp(8080, "localhost") .withHttpWebSocketApp(wsb => Router("/" -> wsRoutes(wsb)).orNotFound) .serve @@ -164,14 +164,14 @@ import sttp.tapir.PublicEndpoint import sttp.tapir.ztapir._ import org.http4s.HttpRoutes import zio.{Task, ZIO} -import zio.stream.Stream +import zio.stream.{Stream, ZStream} val sseEndpoint: PublicEndpoint[Unit, Unit, Stream[Throwable, ServerSentEvent], ZioStreams] = endpoint.get.out(serverSentEventsBody) val routes: HttpRoutes[Task] = ZHttp4sServerInterpreter() - .from(sseEndpoint.zServerLogic(_ => ZIO.succeed(Stream(ServerSentEvent(Some("data"), None, None, None))))) + .from(sseEndpoint.zServerLogic(_ => ZIO.succeed(ZStream(ServerSentEvent(Some("data"), None, None, None))))) .toRoutes ``` diff --git a/generated-doc/out/server/ziohttp.md b/generated-doc/out/server/ziohttp.md index ec7dc306c6..82362b3d5c 100644 --- a/generated-doc/out/server/ziohttp.md +++ b/generated-doc/out/server/ziohttp.md @@ -9,13 +9,13 @@ The `*-zio` modules depend on ZIO 2.x. For ZIO 1.x support, use modules with the You'll need the following dependency for the `ZServerEndpoint` type alias and helper classes: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-zio" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-zio" % "1.0.0-RC3" ``` or just add the zio-http integration which already depends on `tapir-zio`: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-zio-http-server" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-zio-http-server" % "1.0.0-RC3" ``` Next, instead of the usual `import sttp.tapir._`, you should import (or extend the `ZTapir` trait, see [MyTapir](../mytapir.md)): diff --git a/generated-doc/out/stability.md b/generated-doc/out/stability.md new file mode 100644 index 0000000000..4e735def70 --- /dev/null +++ b/generated-doc/out/stability.md @@ -0,0 +1,100 @@ +# Stability of modules + +The modules are categorised using the following levels: + +* **stable**: binary compatibility is guaranteed across major versions; adheres to semantic versioning +* **stabilising**: the API is mostly stable, with rare binary-incompatible changes possible in minor releases (only if necessary) +* **experimental**: API can change significantly even in patch releases + +## Main modules + +| Module | Level | +|----------------|-------------| +| core (Scala 2) | stabilising | +| core (Scala 3) | stabilising | +| server-core | stabilising | +| client-core | stabilising | + +## Server interpreters + +| Module | Level | +|-----------|--------------| +| akka-http | stabilising | +| armeria | stabilising | +| finatra | stabilising | +| http4s | stabilising | +| netty | experimental | +| play | stabilising | +| vertx | stabilising | +| zio1-http | experimental | +| zio-http | experimental | + +## Client interpreters + +| Module | Level | +|--------|-------------| +| sttp | stabilising | +| play | stabilising | +| http4s | stabilising | + +## Documentation interpreters + +| Module | Level | +|----------|-------------| +| openapi | stabilising | +| asyncapi | stabilising | + +## Serverless interpreters + +| Module | Level | +|---------------|--------------| +| aws-lambda | experimental | +| aws-sam | experimental | +| aws-terraform | experimental | + +## Integration modules + +| Module | Level | +|------------|--------------| +| cats | stabilising | +| derevo | stabilising | +| enumeratum | stabilising | +| newtype | stabilising | +| refined | stabilising | +| zio | experimental | +| zio1 | stabilising | + +## JSON modules + +| Module | Level | +|------------|--------------| +| circe | stabilising | +| json4s | stabilising | +| jsoniter | stabilising | +| play-json | stabilising | +| spray-json | stabilising | +| tethys | stabilising | +| upickle | stabilising | +| zio-json | experimental | +| zio1-json | experimental | + +## Testing modules + +| Module | Level | +|-----------|--------------| +| testing | stabilising | +| sttp-mock | experimental | +| sttp-stub | stabilising | + +## Observability modules + +| Module | Level | +|-----------------------|-------------| +| opentelemetry-metrics | stabilising | +| prometheus-metrics | stabilising | + +## Other modules + +| Module | Level | +|--------------------|--------------| +| openapi-codegen | experimental | diff --git a/generated-doc/out/testing.md b/generated-doc/out/testing.md index 02fe5ab066..8c425ef7ba 100644 --- a/generated-doc/out/testing.md +++ b/generated-doc/out/testing.md @@ -23,7 +23,7 @@ Tapir builds upon the `SttpBackendStub` to enable stubbing using `Endpoint`s or dependency: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-sttp-stub-server" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-sttp-stub-server" % "1.0.0-RC3" ``` Let's assume you are using the [akka http](server/akkahttp.md) interpreter. Given the following server endpoint: @@ -140,7 +140,7 @@ requests matching an endpoint, you can use the tapir `SttpBackendStub` extension Similarly as when testing server interpreters, add the dependency: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-sttp-stub-server" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-sttp-stub-server" % "1.0.0-RC3" ``` And the following imports: @@ -195,7 +195,7 @@ with [mock-server](https://www.mock-server.com/) Add the following dependency: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-sttp-mock-server" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-sttp-mock-server" % "1.0.0-RC3" ``` Imports: @@ -266,7 +266,7 @@ result == out To use, add the following dependency: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-testing" % "1.0.0-RC2" +"com.softwaremill.sttp.tapir" %% "tapir-testing" % "1.0.0-RC3" ``` ### Shadowed endpoints @@ -292,7 +292,7 @@ Results in: ```scala res.toString -// res2: String = "Set(GET /x, is shadowed by: GET /x/*, GET /x/y/x, is shadowed by: GET /x/*)" +// res2: String = "Set(GET /x/y/x, is shadowed by: GET /x/*, GET /x, is shadowed by: GET /x/*)" ``` Example 2: diff --git a/integrations/refined/src/main/scala/sttp/tapir/codec/refined/TapirCodecRefined.scala b/integrations/refined/src/main/scala/sttp/tapir/codec/refined/TapirCodecRefined.scala index fe2c164e20..0637a0277e 100644 --- a/integrations/refined/src/main/scala/sttp/tapir/codec/refined/TapirCodecRefined.scala +++ b/integrations/refined/src/main/scala/sttp/tapir/codec/refined/TapirCodecRefined.scala @@ -7,7 +7,6 @@ import eu.timepit.refined.internal.WitnessAs import eu.timepit.refined.numeric.{Greater, GreaterEqual, Less, LessEqual} import eu.timepit.refined.refineV import eu.timepit.refined.string.{MatchesRegex, Uuid} -import shapeless.Witness import sttp.tapir._ import scala.reflect.ClassTag @@ -48,10 +47,10 @@ trait TapirCodecRefined extends LowPriorityValidatorForPredicate { implicit val validatorForNonEmptyString: PrimitiveValidatorForPredicate[String, NonEmpty] = ValidatorForPredicate.fromPrimitiveValidator[String, NonEmpty](Validator.minLength(1)) - implicit def validatorForMatchesRegexp[S <: String](implicit - ws: Witness.Aux[S] + implicit def validatorForMatchesRegexpString[S <: String](implicit + ws: WitnessAs[S, String] ): PrimitiveValidatorForPredicate[String, MatchesRegex[S]] = - ValidatorForPredicate.fromPrimitiveValidator(Validator.pattern(ws.value)) + ValidatorForPredicate.fromPrimitiveValidator(Validator.pattern(ws.snd)) implicit def validatorForMaxSizeOnString[T <: String, NM](implicit ws: WitnessAs[NM, Int] @@ -159,7 +158,7 @@ trait LowPriorityValidatorForPredicate { override val validator: Validator.Custom[V] = Validator.Custom( { v => if (refinedValidator.isValid(v)) ValidationResult.Valid - else ValidationResult.Invalid(Some(implicitly[ClassTag[P]].runtimeClass.toString)) + else ValidationResult.Invalid(implicitly[ClassTag[P]].runtimeClass.toString) } ) // for the moment there is no way to get a human description of a predicate/validator without having a concrete value to run it diff --git a/integrations/refined/src/test/scala-2/sttp/tapir/codec/refined/TapirCodecRefinedTestScala2.scala b/integrations/refined/src/test/scala-2/sttp/tapir/codec/refined/TapirCodecRefinedTestScala2.scala new file mode 100644 index 0000000000..714873be80 --- /dev/null +++ b/integrations/refined/src/test/scala-2/sttp/tapir/codec/refined/TapirCodecRefinedTestScala2.scala @@ -0,0 +1,180 @@ +package sttp.tapir.codec.refined + +import eu.timepit.refined.api.Refined +import eu.timepit.refined.boolean.Or +import eu.timepit.refined.collection.{MaxSize, MinSize, NonEmpty} +import eu.timepit.refined.numeric.{Greater, GreaterEqual, Interval, Less, LessEqual} +import eu.timepit.refined.string.MatchesRegex +import eu.timepit.refined.types.string.NonEmptyString +import eu.timepit.refined.W +import eu.timepit.refined.refineMV +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers +import sttp.tapir.Codec.PlainCodec +import sttp.tapir.{DecodeResult, Schema, ValidationError, Validator} + +import scala.annotation.nowarn + +class TapirCodecRefinedTestScala2 extends AnyFlatSpec with Matchers with TapirCodecRefined { + + val nonEmptyStringCodec: PlainCodec[NonEmptyString] = implicitly[PlainCodec[NonEmptyString]] + + "Generated codec" should "correctly delegate to raw parser and refine it" in { + nonEmptyStringCodec.decode("vive le fromage") shouldBe DecodeResult.Value(refineMV[NonEmpty]("vive le fromage")) + } + + "Generated codec for MatchesRegex" should "use tapir Validator.Pattern" in { + type VariableConstraint = MatchesRegex[W.`"[a-zA-Z][-a-zA-Z0-9_]*"`.T] + type VariableString = String Refined VariableConstraint + val identifierCodec = implicitly[PlainCodec[VariableString]] + + val expectedValidator: Validator[String] = Validator.pattern("[a-zA-Z][-a-zA-Z0-9_]*") + identifierCodec.decode("-bad") should matchPattern { + case DecodeResult.InvalidValue(List(ValidationError(validator, "-bad", _, _))) if validator == expectedValidator => + } + } + + it should "decode value matching pattern" in { + type VariableConstraint = MatchesRegex[W.`"[a-zA-Z][-a-zA-Z0-9_]*"`.T] + type VariableString = String Refined VariableConstraint + val identifierCodec = implicitly[PlainCodec[VariableString]] + identifierCodec.decode("ok") shouldBe DecodeResult.Value(refineMV[VariableConstraint]("ok")) + } + + "Generated codec for MaxSize on string" should "use tapir Validator.maxLength" in { + type VariableConstraint = MaxSize[W.`2`.T] + type VariableString = String Refined VariableConstraint + val identifierCodec = implicitly[PlainCodec[VariableString]] + + val expectedValidator: Validator[String] = Validator.maxLength(2) + identifierCodec.decode("bad") should matchPattern { + case DecodeResult.InvalidValue(List(ValidationError(validator, "bad", _, _))) if validator == expectedValidator => + } + } + + "Generated codec for MinSize on string" should "use tapir Validator.minLength" in { + type VariableConstraint = MinSize[W.`42`.T] + type VariableString = String Refined VariableConstraint + val identifierCodec = implicitly[PlainCodec[VariableString]] + + val expectedValidator: Validator[String] = Validator.minLength(42) + identifierCodec.decode("bad") should matchPattern { + case DecodeResult.InvalidValue(List(ValidationError(validator, "bad", _, _))) if validator == expectedValidator => + } + } + + "Generated codec for Less" should "use tapir Validator.max" in { + type IntConstraint = Less[W.`3`.T] + type LimitedInt = Int Refined IntConstraint + val limitedIntCodec = implicitly[PlainCodec[LimitedInt]] + + val expectedValidator: Validator[Int] = Validator.max(3, exclusive = true) + limitedIntCodec.decode("3") should matchPattern { + case DecodeResult.InvalidValue(List(ValidationError(validator, 3, _, _))) if validator == expectedValidator => + } + } + + "Generated codec for LessEqual" should "use tapir Validator.max" in { + type IntConstraint = LessEqual[W.`3`.T] + type LimitedInt = Int Refined IntConstraint + val limitedIntCodec = implicitly[PlainCodec[LimitedInt]] + + val expectedValidator: Validator[Int] = Validator.max(3) + limitedIntCodec.decode("4") should matchPattern { + case DecodeResult.InvalidValue(List(ValidationError(validator, 4, _, _))) if validator == expectedValidator => + } + } + + "Generated codec for Greater" should "use tapir Validator.min" in { + type IntConstraint = Greater[W.`3`.T] + type LimitedInt = Int Refined IntConstraint + val limitedIntCodec = implicitly[PlainCodec[LimitedInt]] + + val expectedValidator: Validator[Int] = Validator.min(3, exclusive = true) + limitedIntCodec.decode("3") should matchPattern { + case DecodeResult.InvalidValue(List(ValidationError(validator, 3, _, _))) if validator == expectedValidator => + } + } + + "Generated codec for GreaterEqual" should "use tapir Validator.min" in { + type IntConstraint = GreaterEqual[W.`3`.T] + type LimitedInt = Int Refined IntConstraint + val limitedIntCodec = implicitly[PlainCodec[LimitedInt]] + + val expectedValidator: Validator[Int] = Validator.min(3) + limitedIntCodec.decode("2") should matchPattern { + case DecodeResult.InvalidValue(List(ValidationError(validator, 2, _, _))) if validator == expectedValidator => + } + } + + "Generated validator for Greater" should "use tapir Validator.min" in { + type IntConstraint = Greater[W.`3`.T] + type LimitedInt = Int Refined IntConstraint + + implicitly[Schema[LimitedInt]].validator should matchPattern { case Validator.Mapped(Validator.Min(3, true), _) => + } + } + + "Generated validator for Interval.Open" should "use tapir Validator.min and Validator.max" in { + type IntConstraint = Interval.Open[W.`1`.T, W.`3`.T] + type LimitedInt = Int Refined IntConstraint + + implicitly[Schema[LimitedInt]].validator should matchPattern { + case Validator.Mapped(Validator.All(List(Validator.Min(1, true), Validator.Max(3, true))), _) => + } + } + + "Generated validator for Interval.Close" should "use tapir Validator.min and Validator.max" in { + type IntConstraint = Interval.Closed[W.`1`.T, W.`3`.T] + type LimitedInt = Int Refined IntConstraint + + implicitly[Schema[LimitedInt]].validator should matchPattern { + case Validator.Mapped(Validator.All(List(Validator.Min(1, false), Validator.Max(3, false))), _) => + } + } + + "Generated validator for Interval.OpenClose" should "use tapir Validator.min and Validator.max" in { + type IntConstraint = Interval.OpenClosed[W.`1`.T, W.`3`.T] + type LimitedInt = Int Refined IntConstraint + + implicitly[Schema[LimitedInt]].validator should matchPattern { + case Validator.Mapped(Validator.All(List(Validator.Min(1, true), Validator.Max(3, false))), _) => + } + } + + "Generated validator for Interval.ClosedOpen" should "use tapir Validator.min and Validator.max" in { + type IntConstraint = Interval.ClosedOpen[W.`1`.T, W.`3`.T] + type LimitedInt = Int Refined IntConstraint + + implicitly[Schema[LimitedInt]].validator should matchPattern { + case Validator.Mapped(Validator.All(List(Validator.Min(1, false), Validator.Max(3, true))), _) => + } + } + + "Generate validator for Or" should "use tapir Validator.any" in { + type IntConstraint = Greater[W.`3`.T] Or Less[W.` -3`.T] + type LimitedInt = Int Refined IntConstraint + implicitly[Schema[LimitedInt]].validator should matchPattern { + case Validator.Mapped(Validator.Any(List(Validator.Min(3, true), Validator.Max(-3, true))), _) => + } + } + + "TapirCodecRefined" should "compile using implicit schema for refined types" in { + import io.circe.refined._ + import sttp.tapir + import sttp.tapir._ + import sttp.tapir.json.circe._ + + @nowarn // we only want to ensure it compiles but it warns because it is not used + object TapirCodecRefinedDeepImplicitSearch extends TapirCodecRefined with TapirJsonCirce { + type StringConstraint = MatchesRegex[W.`"[^\u0000-\u001f]{1,29}"`.T] + type LimitedString = String Refined StringConstraint + + val refinedEndpoint: PublicEndpoint[(LimitedString, List[LimitedString]), Unit, List[Option[LimitedString]], Nothing] = + tapir.endpoint.post + .in(path[LimitedString]("ls") / jsonBody[List[LimitedString]]) + .out(jsonBody[List[Option[LimitedString]]]) + } + } + +} diff --git a/integrations/refined/src/test/scala-3/sttp/tapir/codec/refined/TapirCodecRefinedTestScala3.scala b/integrations/refined/src/test/scala-3/sttp/tapir/codec/refined/TapirCodecRefinedTestScala3.scala new file mode 100644 index 0000000000..4c678673dc --- /dev/null +++ b/integrations/refined/src/test/scala-3/sttp/tapir/codec/refined/TapirCodecRefinedTestScala3.scala @@ -0,0 +1,182 @@ +package sttp.tapir.codec.refined + +import eu.timepit.refined.api.Refined +import eu.timepit.refined.boolean.Or +import eu.timepit.refined.collection.{MaxSize, MinSize, NonEmpty} +import eu.timepit.refined.numeric.{Greater, GreaterEqual, Interval, Less, LessEqual, Negative, NonNegative, NonPositive, Positive} +import eu.timepit.refined.string.{IPv4, MatchesRegex} +import eu.timepit.refined.types.string.NonEmptyString +import eu.timepit.refined.refineV +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers +import sttp.tapir.Codec.PlainCodec +import sttp.tapir.{DecodeResult, Schema, ValidationError, Validator} + +class TapirCodecRefinedTestScala3 extends AnyFlatSpec with Matchers with TapirCodecRefined { + + val nonEmptyStringCodec: PlainCodec[NonEmptyString] = implicitly[PlainCodec[NonEmptyString]] + + "Generated codec" should "correctly delegate to raw parser and refine it" in { + refineV[NonEmpty]("vive le fromage") match { + case Right(nes) => nonEmptyStringCodec.decode("vive le fromage") shouldBe DecodeResult.Value(nes) + case Left(_) => fail() + } + } + + "Generated codec for MatchesRegex" should "use tapir Validator.Pattern" in { + type VariableConstraint = MatchesRegex["[a-zA-Z][-a-zA-Z0-9_]*"] + type VariableString = String Refined VariableConstraint + val identifierCodec = implicitly[PlainCodec[VariableString]] + + val expectedValidator: Validator[String] = Validator.pattern("[a-zA-Z][-a-zA-Z0-9_]*") + identifierCodec.decode("-bad") should matchPattern { + case DecodeResult.InvalidValue(List(ValidationError(validator, "-bad", _, _))) if validator == expectedValidator => + } + } + + it should "decode value matching pattern" in { + type VariableConstraint = MatchesRegex["[a-zA-Z][-a-zA-Z0-9_]*"] + type VariableString = String Refined VariableConstraint + val identifierCodec = implicitly[PlainCodec[VariableString]] + refineV[VariableConstraint]("ok") match { + case Right(s) => identifierCodec.decode("ok") shouldBe DecodeResult.Value(s) + case Left(_) => fail() + } + } + + "Generated codec for MaxSize on string" should "use tapir Validator.maxLength" in { + type VariableConstraint = MaxSize[2] + type VariableString = String Refined VariableConstraint + val identifierCodec = implicitly[PlainCodec[VariableString]] + + val expectedValidator: Validator[String] = Validator.maxLength(2) + identifierCodec.decode("bad") should matchPattern { + case DecodeResult.InvalidValue(List(ValidationError(validator, "bad", _, _))) if validator == expectedValidator => + } + } + + "Generated codec for MinSize on string" should "use tapir Validator.minLength" in { + type VariableConstraint = MinSize[42] + type VariableString = String Refined VariableConstraint + val identifierCodec = implicitly[PlainCodec[VariableString]] + + val expectedValidator: Validator[String] = Validator.minLength(42) + identifierCodec.decode("bad") should matchPattern { + case DecodeResult.InvalidValue(List(ValidationError(validator, "bad", _, _))) if validator == expectedValidator => + } + } + + "Generated codec for Less" should "use tapir Validator.max" in { + type IntConstraint = Less[3] + type LimitedInt = Int Refined IntConstraint + val limitedIntCodec = implicitly[PlainCodec[LimitedInt]] + + val expectedValidator: Validator[Int] = Validator.max(3, exclusive = true) + limitedIntCodec.decode("3") should matchPattern { + case DecodeResult.InvalidValue(List(ValidationError(validator, 3, _, _))) if validator == expectedValidator => + } + } + + "Generated codec for LessEqual" should "use tapir Validator.max" in { + type IntConstraint = LessEqual[3] + type LimitedInt = Int Refined IntConstraint + val limitedIntCodec = implicitly[PlainCodec[LimitedInt]] + + val expectedValidator: Validator[Int] = Validator.max(3) + limitedIntCodec.decode("4") should matchPattern { + case DecodeResult.InvalidValue(List(ValidationError(validator, 4, _, _))) if validator == expectedValidator => + } + } + + "Generated codec for Greater" should "use tapir Validator.min" in { + type IntConstraint = Greater[3] + type LimitedInt = Int Refined IntConstraint + val limitedIntCodec = implicitly[PlainCodec[LimitedInt]] + + val expectedValidator: Validator[Int] = Validator.min(3, exclusive = true) + limitedIntCodec.decode("3") should matchPattern { + case DecodeResult.InvalidValue(List(ValidationError(validator, 3, _, _))) if validator == expectedValidator => + } + } + + "Generated codec for GreaterEqual" should "use tapir Validator.min" in { + type IntConstraint = GreaterEqual[3] + type LimitedInt = Int Refined IntConstraint + val limitedIntCodec = implicitly[PlainCodec[LimitedInt]] + + val expectedValidator: Validator[Int] = Validator.min(3) + limitedIntCodec.decode("2") should matchPattern { + case DecodeResult.InvalidValue(List(ValidationError(validator, 2, _, _))) if validator == expectedValidator => + } + } + + "Generated validator for Greater" should "use tapir Validator.min" in { + type IntConstraint = Greater[3] + type LimitedInt = Int Refined IntConstraint + + implicitly[Schema[LimitedInt]].validator should matchPattern { case Validator.Mapped(Validator.Min(3, true), _) => + } + } + + "Generated validator for Interval.Open" should "use tapir Validator.min and Validator.max" in { + type IntConstraint = Interval.Open[1, 3] + type LimitedInt = Int Refined IntConstraint + + implicitly[Schema[LimitedInt]].validator should matchPattern { + case Validator.Mapped(Validator.All(List(Validator.Min(1, true), Validator.Max(3, true))), _) => + } + } + + "Generated validator for Interval.Close" should "use tapir Validator.min and Validator.max" in { + type IntConstraint = Interval.Closed[1, 3] + type LimitedInt = Int Refined IntConstraint + + implicitly[Schema[LimitedInt]].validator should matchPattern { + case Validator.Mapped(Validator.All(List(Validator.Min(1, false), Validator.Max(3, false))), _) => + } + } + + "Generated validator for Interval.OpenClose" should "use tapir Validator.min and Validator.max" in { + type IntConstraint = Interval.OpenClosed[1, 3] + type LimitedInt = Int Refined IntConstraint + + implicitly[Schema[LimitedInt]].validator should matchPattern { + case Validator.Mapped(Validator.All(List(Validator.Min(1, true), Validator.Max(3, false))), _) => + } + } + + "Generated validator for Interval.ClosedOpen" should "use tapir Validator.min and Validator.max" in { + type IntConstraint = Interval.ClosedOpen[1, 3] + type LimitedInt = Int Refined IntConstraint + + implicitly[Schema[LimitedInt]].validator should matchPattern { + case Validator.Mapped(Validator.All(List(Validator.Min(1, false), Validator.Max(3, true))), _) => + } + } + + "Generate validator for Or" should "use tapir Validator.any" in { + type IntConstraint = Greater[3] Or Less[-3] + type LimitedInt = Int Refined IntConstraint + implicitly[Schema[LimitedInt]].validator should matchPattern { + case Validator.Mapped(Validator.Any(List(Validator.Min(3, true), Validator.Max(-3, true))), _) => + } + } + + "TapirCodecRefined" should "compile using implicit schema for refined types" in { + import io.circe.refined._ + import sttp.tapir + import sttp.tapir._ + import sttp.tapir.json.circe._ + + object TapirCodecRefinedDeepImplicitSearch extends TapirCodecRefined with TapirJsonCirce { + type StringConstraint = MatchesRegex["[^\u0000-\u001f]{1,29}"] + type LimitedString = String Refined StringConstraint + + val refinedEndpoint: PublicEndpoint[(LimitedString, List[LimitedString]), Unit, List[Option[LimitedString]], Nothing] = + tapir.endpoint.post + .in(path[LimitedString]("ls") / jsonBody[List[LimitedString]]) + .out(jsonBody[List[Option[LimitedString]]]) + } + } + +} diff --git a/integrations/refined/src/test/scala/sttp/tapir/codec/refined/TapirCodecRefinedTest.scala b/integrations/refined/src/test/scala/sttp/tapir/codec/refined/TapirCodecRefinedTest.scala index f1d85b353f..e310ccc88b 100644 --- a/integrations/refined/src/test/scala/sttp/tapir/codec/refined/TapirCodecRefinedTest.scala +++ b/integrations/refined/src/test/scala/sttp/tapir/codec/refined/TapirCodecRefinedTest.scala @@ -1,12 +1,10 @@ package sttp.tapir.codec.refined import eu.timepit.refined.api.Refined -import eu.timepit.refined.boolean.Or -import eu.timepit.refined.collection.{MaxSize, MinSize, NonEmpty} -import eu.timepit.refined.numeric.{Greater, GreaterEqual, Interval, Less, LessEqual, Negative, NonNegative, NonPositive, Positive} -import eu.timepit.refined.string.{IPv4, MatchesRegex} +import eu.timepit.refined.numeric.{Negative, NonNegative, NonPositive, Positive} +import eu.timepit.refined.string.IPv4 import eu.timepit.refined.types.string.NonEmptyString -import eu.timepit.refined.{W, refineMV, refineV} +import eu.timepit.refined.refineV import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers import sttp.tapir.Codec.PlainCodec @@ -23,105 +21,16 @@ class TapirCodecRefinedTest extends AnyFlatSpec with Matchers with TapirCodecRef } } - it should "correctly delegate to raw parser and refine it" in { - nonEmptyStringCodec.decode("vive le fromage") shouldBe DecodeResult.Value(refineMV[NonEmpty]("vive le fromage")) - } - it should "return DecodeResult.Invalid if subtype can't be refined with derived tapir validator if non tapir validator available" in { type IPString = String Refined IPv4 val IPStringCodec = implicitly[PlainCodec[IPString]] - val expectedMsg = refineV[IPv4]("192.168.0.1000").left.get + val expectedMsg = refineV[IPv4]("192.168.0.1000").swap.getOrElse(throw new Exception("A Left was expected but got a Right")) IPStringCodec.decode("192.168.0.1000") should matchPattern { case DecodeResult.InvalidValue(List(ValidationError(_, "192.168.0.1000", _, Some(`expectedMsg`)))) => } } - "Generated codec for MatchesRegex" should "use tapir Validator.Pattern" in { - type VariableConstraint = MatchesRegex[W.`"[a-zA-Z][-a-zA-Z0-9_]*"`.T] - type VariableString = String Refined VariableConstraint - val identifierCodec = implicitly[PlainCodec[VariableString]] - - val expectedValidator: Validator[String] = Validator.pattern("[a-zA-Z][-a-zA-Z0-9_]*") - identifierCodec.decode("-bad") should matchPattern { - case DecodeResult.InvalidValue(List(ValidationError(validator, "-bad", _, _))) if validator == expectedValidator => - } - } - - "Generated codec for MaxSize on string" should "use tapir Validator.maxLength" in { - type VariableConstraint = MaxSize[W.`2`.T] - type VariableString = String Refined VariableConstraint - val identifierCodec = implicitly[PlainCodec[VariableString]] - - val expectedValidator: Validator[String] = Validator.maxLength(2) - identifierCodec.decode("bad") should matchPattern { - case DecodeResult.InvalidValue(List(ValidationError(validator, "bad", _, _))) if validator == expectedValidator => - } - } - - "Generated codec for MinSize on string" should "use tapir Validator.minLength" in { - type VariableConstraint = MinSize[W.`42`.T] - type VariableString = String Refined VariableConstraint - val identifierCodec = implicitly[PlainCodec[VariableString]] - - val expectedValidator: Validator[String] = Validator.minLength(42) - identifierCodec.decode("bad") should matchPattern { - case DecodeResult.InvalidValue(List(ValidationError(validator, "bad", _, _))) if validator == expectedValidator => - } - } - - "Generated codec for Less" should "use tapir Validator.max" in { - type IntConstraint = Less[W.`3`.T] - type LimitedInt = Int Refined IntConstraint - val limitedIntCodec = implicitly[PlainCodec[LimitedInt]] - - val expectedValidator: Validator[Int] = Validator.max(3, exclusive = true) - limitedIntCodec.decode("3") should matchPattern { - case DecodeResult.InvalidValue(List(ValidationError(validator, 3, _, _))) if validator == expectedValidator => - } - } - - "Generated codec for LessEqual" should "use tapir Validator.max" in { - type IntConstraint = LessEqual[W.`3`.T] - type LimitedInt = Int Refined IntConstraint - val limitedIntCodec = implicitly[PlainCodec[LimitedInt]] - - val expectedValidator: Validator[Int] = Validator.max(3) - limitedIntCodec.decode("4") should matchPattern { - case DecodeResult.InvalidValue(List(ValidationError(validator, 4, _, _))) if validator == expectedValidator => - } - } - - "Generated codec for Greater" should "use tapir Validator.min" in { - type IntConstraint = Greater[W.`3`.T] - type LimitedInt = Int Refined IntConstraint - val limitedIntCodec = implicitly[PlainCodec[LimitedInt]] - - val expectedValidator: Validator[Int] = Validator.min(3, exclusive = true) - limitedIntCodec.decode("3") should matchPattern { - case DecodeResult.InvalidValue(List(ValidationError(validator, 3, _, _))) if validator == expectedValidator => - } - } - - "Generated codec for GreaterEqual" should "use tapir Validator.min" in { - type IntConstraint = GreaterEqual[W.`3`.T] - type LimitedInt = Int Refined IntConstraint - val limitedIntCodec = implicitly[PlainCodec[LimitedInt]] - - val expectedValidator: Validator[Int] = Validator.min(3) - limitedIntCodec.decode("2") should matchPattern { - case DecodeResult.InvalidValue(List(ValidationError(validator, 2, _, _))) if validator == expectedValidator => - } - } - - "Generated validator for Greater" should "use tapir Validator.min" in { - type IntConstraint = Greater[W.`3`.T] - type LimitedInt = Int Refined IntConstraint - - implicitly[Schema[LimitedInt]].validator should matchPattern { case Validator.Mapped(Validator.Min(3, true), _) => - } - } - "Generated validator for Positive" should "use tapir Validator.min" in { type IntConstraint = Positive type LimitedInt = Int Refined IntConstraint @@ -150,67 +59,6 @@ class TapirCodecRefinedTest extends AnyFlatSpec with Matchers with TapirCodecRef implicitly[Schema[LimitedInt]].validator should matchPattern { case Validator.Mapped(Validator.Max(0, true), _) => } } - "Generated validator for Interval.Open" should "use tapir Validator.min and Validator.max" in { - type IntConstraint = Interval.Open[W.`1`.T, W.`3`.T] - type LimitedInt = Int Refined IntConstraint - - implicitly[Schema[LimitedInt]].validator should matchPattern { - case Validator.Mapped(Validator.All(List(Validator.Min(1, true), Validator.Max(3, true))), _) => - } - } - - "Generated validator for Interval.Close" should "use tapir Validator.min and Validator.max" in { - type IntConstraint = Interval.Closed[W.`1`.T, W.`3`.T] - type LimitedInt = Int Refined IntConstraint - - implicitly[Schema[LimitedInt]].validator should matchPattern { - case Validator.Mapped(Validator.All(List(Validator.Min(1, false), Validator.Max(3, false))), _) => - } - } - - "Generated validator for Interval.OpenClose" should "use tapir Validator.min and Validator.max" in { - type IntConstraint = Interval.OpenClosed[W.`1`.T, W.`3`.T] - type LimitedInt = Int Refined IntConstraint - - implicitly[Schema[LimitedInt]].validator should matchPattern { - case Validator.Mapped(Validator.All(List(Validator.Min(1, true), Validator.Max(3, false))), _) => - } - } - - "Generated validator for Interval.ClosedOpen" should "use tapir Validator.min and Validator.max" in { - type IntConstraint = Interval.ClosedOpen[W.`1`.T, W.`3`.T] - type LimitedInt = Int Refined IntConstraint - - implicitly[Schema[LimitedInt]].validator should matchPattern { - case Validator.Mapped(Validator.All(List(Validator.Min(1, false), Validator.Max(3, true))), _) => - } - } - - "Generate validator for Or" should "use tapir Validator.any" in { - type IntConstraint = Greater[W.`3`.T] Or Less[W.` -3`.T] - type LimitedInt = Int Refined IntConstraint - implicitly[Schema[LimitedInt]].validator should matchPattern { - case Validator.Mapped(Validator.Any(List(Validator.Min(3, true), Validator.Max(-3, true))), _) => - } - } - - "TapirCodecRefined" should "compile using implicit schema for refined types" in { - import io.circe.refined._ - import sttp.tapir - import sttp.tapir._ - import sttp.tapir.json.circe._ - - object TapirCodecRefinedDeepImplicitSearch extends TapirCodecRefined with TapirJsonCirce { - type StringConstraint = MatchesRegex[W.`"[^\u0000-\u001f]{1,29}"`.T] - type LimitedString = String Refined StringConstraint - - val refinedEndpoint: PublicEndpoint[(LimitedString, List[LimitedString]), Unit, List[Option[LimitedString]], Nothing] = - tapir.endpoint.post - .in(path[LimitedString]("ls") / jsonBody[List[LimitedString]]) - .out(jsonBody[List[Option[LimitedString]]]) - } - } - "Using refined" should "compile when using tapir endpoints" in { // this used to cause a: // [error] java.lang.StackOverflowError @@ -220,4 +68,5 @@ class TapirCodecRefinedTest extends AnyFlatSpec with Matchers with TapirCodecRef import sttp.tapir._ endpoint.in("x") } + } diff --git a/integrations/zio/src/main/scala/sttp/tapir/ztapir/RIOMonadError.scala b/integrations/zio/src/main/scala/sttp/tapir/ztapir/RIOMonadError.scala index 881b895b8a..00c4432f95 100644 --- a/integrations/zio/src/main/scala/sttp/tapir/ztapir/RIOMonadError.scala +++ b/integrations/zio/src/main/scala/sttp/tapir/ztapir/RIOMonadError.scala @@ -1,16 +1,16 @@ package sttp.tapir.ztapir import sttp.monad.MonadError -import zio.{RIO, URIO} +import zio.{RIO, ZIO} class RIOMonadError[R] extends MonadError[RIO[R, *]] { - override def unit[T](t: T): RIO[R, T] = URIO.succeed(t) + override def unit[T](t: T): RIO[R, T] = ZIO.succeed(t) override def map[T, T2](fa: RIO[R, T])(f: T => T2): RIO[R, T2] = fa.map(f) override def flatMap[T, T2](fa: RIO[R, T])(f: T => RIO[R, T2]): RIO[R, T2] = fa.flatMap(f) - override def error[T](t: Throwable): RIO[R, T] = RIO.fail(t) + override def error[T](t: Throwable): RIO[R, T] = ZIO.fail(t) override protected def handleWrappedError[T](rt: RIO[R, T])(h: PartialFunction[Throwable, RIO[R, T]]): RIO[R, T] = rt.catchSome(h) - override def eval[T](t: => T): RIO[R, T] = RIO.attempt(t) - override def suspend[T](t: => RIO[R, T]): RIO[R, T] = RIO.suspend(t) + override def eval[T](t: => T): RIO[R, T] = ZIO.attempt(t) + override def suspend[T](t: => RIO[R, T]): RIO[R, T] = ZIO.suspend(t) override def flatten[T](ffa: RIO[R, RIO[R, T]]): RIO[R, T] = ffa.flatten override def ensure[T](f: RIO[R, T], e: => RIO[R, Unit]): RIO[R, T] = f.ensuring(e.ignore) } diff --git a/integrations/zio/src/test/scala/sttp/tapir/ztapir/ZTapirTest.scala b/integrations/zio/src/test/scala/sttp/tapir/ztapir/ZTapirTest.scala index 1a24d39cea..0ae7569154 100644 --- a/integrations/zio/src/test/scala/sttp/tapir/ztapir/ZTapirTest.scala +++ b/integrations/zio/src/test/scala/sttp/tapir/ztapir/ZTapirTest.scala @@ -10,7 +10,6 @@ import sttp.tapir.model.{ConnectionInfo, ServerRequest} import sttp.tapir.server.model.ServerResponse import zio.{UIO, ZIO} import sttp.tapir.ztapir.instances.TestMonadError._ -import zio.test.ZSpec import zio.test._ import zio.test.Assertion._ @@ -20,7 +19,7 @@ import scala.collection.immutable.Seq object ZTapirTest extends ZIOSpecDefault with ZTapir { - def spec: ZSpec[TestEnvironment, Any] = + def spec: Spec[TestEnvironment, Any] = suite("ZTapir tests")(testZServerLogicErrorHandling, testZServerSecurityLogicErrorHandling) type ResponseBodyType = String @@ -69,7 +68,7 @@ object ZTapirTest extends ZIOSpecDefault with ZTapir { } private def errorToResponse(error: Throwable): UIO[RequestResult.Response[ResponseBodyType]] = - UIO.succeed(RequestResult.Response(ServerResponse[ResponseBodyType](StatusCode.InternalServerError, Nil, Some(error.getMessage), None))) + ZIO.succeed(RequestResult.Response(ServerResponse[ResponseBodyType](StatusCode.InternalServerError, Nil, Some(error.getMessage), None))) final case class User(name: String) diff --git a/integrations/zio/src/test/scala/sttp/tapir/ztapir/ZioServerSentEventsTest.scala b/integrations/zio/src/test/scala/sttp/tapir/ztapir/ZioServerSentEventsTest.scala index 486a790276..733018bbce 100644 --- a/integrations/zio/src/test/scala/sttp/tapir/ztapir/ZioServerSentEventsTest.scala +++ b/integrations/zio/src/test/scala/sttp/tapir/ztapir/ZioServerSentEventsTest.scala @@ -9,10 +9,10 @@ import zio.stream._ import java.nio.charset.Charset object ZioServerSentEventsTest extends ZIOSpecDefault { - def spec: ZSpec[TestEnvironment, Any] = + def spec: Spec[TestEnvironment, Any] = suite("ZioServerSentEvents tests")( test("serialiseSSEToBytes should successfully serialise simple Server Sent Event to ByteString") { - val sse: Stream[Nothing, ServerSentEvent] = Stream(ServerSentEvent(Some("data"), Some("event"), Some("id1"), Some(10))) + val sse: Stream[Nothing, ServerSentEvent] = ZStream(ServerSentEvent(Some("data"), Some("event"), Some("id1"), Some(10))) val serialised = ZioServerSentEvents.serialiseSSEToBytes(sse) serialised.runCollect.map { sseEvents => assert(sseEvents.toList)(equalTo(s"""data: data @@ -24,7 +24,7 @@ object ZioServerSentEventsTest extends ZIOSpecDefault { } }, test("serialiseSSEToBytes should omit fields that are not set") { - val sse = Stream(ServerSentEvent(Some("data"), None, Some("id1"), None)) + val sse = ZStream(ServerSentEvent(Some("data"), None, Some("id1"), None)) val serialised = ZioServerSentEvents.serialiseSSEToBytes(sse) serialised.runCollect.map { sseEvents => assert(sseEvents.toList)(equalTo(s"""data: data @@ -34,7 +34,7 @@ object ZioServerSentEventsTest extends ZIOSpecDefault { } }, test("serialiseSSEToBytes should successfully serialise multiline data event") { - val sse = Stream( + val sse = ZStream( ServerSentEvent( Some("""some data info 1 |some data info 2 @@ -54,7 +54,7 @@ object ZioServerSentEventsTest extends ZIOSpecDefault { } }, test("parseBytesToSSE should successfully parse SSE bytes to SSE structure") { - val sseBytes = Stream.fromChunk( + val sseBytes = ZStream.fromChunk( Chunk.fromArray( """data: event1 data |event: event1 diff --git a/project/Versions.scala b/project/Versions.scala index 0c1deade23..9e2a7cac0a 100644 --- a/project/Versions.scala +++ b/project/Versions.scala @@ -3,13 +3,13 @@ object Versions { val catsEffect = "3.3.12" val circe = "0.14.1" val circeYaml = "0.14.1" - val sttp = "3.5.2" + val sttp = "3.6.2" val sttpModel = "1.4.26" - val sttpShared = "1.3.5" + val sttpShared = "1.3.6" val sttpApispec = "0.2.1" val akkaHttp = "10.2.9" val akkaStreams = "2.6.19" - val swaggerUi = "4.10.3" + val swaggerUi = "4.11.1" val upickle = "2.0.0" val playJson = "2.9.2" val finatra = "22.4.0" @@ -25,10 +25,10 @@ object Versions { val zio1InteropCats = "3.2.9.1" val zio1Json = "0.2.0-M4" val zio1InteropReactiveStreams = "1.3.12" - val zio = "2.0.0-RC5" - val zioInteropCats = "3.3.0-RC6" - val zioInteropReactiveStreams = "2.0.0-RC6" - val zioJson = "0.3.0-RC7" + val zio = "2.0.0-RC6" + val zioInteropCats = "3.3.0-RC7" + val zioInteropReactiveStreams = "2.0.0-RC7" + val zioJson = "0.3.0-RC8" val playClient = "2.1.10" val playServer = "2.8.15" val tethys = "0.26.0" diff --git a/server/armeria-server/src/main/scala/sttp/tapir/server/armeria/RouteMapping.scala b/server/armeria-server/src/main/scala/sttp/tapir/server/armeria/RouteMapping.scala index eac0e763e4..eb1e8505a8 100644 --- a/server/armeria-server/src/main/scala/sttp/tapir/server/armeria/RouteMapping.scala +++ b/server/armeria-server/src/main/scala/sttp/tapir/server/armeria/RouteMapping.scala @@ -4,8 +4,8 @@ import com.linecorp.armeria.server.Route import sttp.tapir.EndpointIO.{Body, StreamBodyWrapper} import sttp.tapir.EndpointInput.{FixedPath, PathCapture, PathsCapture} import sttp.tapir.RawBodyType.FileBody -import sttp.tapir.internal.{RichEndpoint, RichEndpointOutput} -import sttp.tapir.{AnyEndpoint, EndpointInput, EndpointTransput, RawBodyType} +import sttp.tapir.internal.{RichEndpoint, RichEndpointInput, RichEndpointOutput} +import sttp.tapir.{AnyEndpoint, EndpointInput, EndpointTransput, RawBodyType, noTrailingSlash} private[armeria] object RouteMapping { @@ -22,7 +22,14 @@ private[armeria] object RouteMapping { case (true, true) => ExchangeType.BidiStreaming } - toPathPatterns(inputs).map { path => + val hasNoTrailingSlash = e.securityInput + .and(e.input) + .traverseInputs { + case i if i == noTrailingSlash => Vector(()) + } + .nonEmpty + + toPathPatterns(inputs, hasNoTrailingSlash).map { path => // Allows all HTTP method to handle invalid requests by RejectInterceptor val routeBuilder = Route @@ -44,7 +51,7 @@ private[armeria] object RouteMapping { case _ => false } - private def toPathPatterns(inputs: Seq[EndpointInput.Basic[_]]): List[String] = { + private def toPathPatterns(inputs: Seq[EndpointInput.Basic[_]], hasNoTrailingSlash: Boolean): List[String] = { var idxUsed = 0 var capturePaths = false val fragments = inputs.collect { @@ -68,8 +75,11 @@ private[armeria] object RouteMapping { if (capturePaths) { List(pathPattern) } else { - // endpoint.in("api") should match both '/api', '/api/' - List(pathPattern, s"$pathPattern/") + if (hasNoTrailingSlash) List(pathPattern) + else { + // endpoint.in("api") should match both '/api', '/api/' + List(pathPattern, s"$pathPattern/") + } } } } diff --git a/server/armeria-server/zio/src/main/scala/sttp/tapir/server/armeria/zio/ArmeriaZioServerOptions.scala b/server/armeria-server/zio/src/main/scala/sttp/tapir/server/armeria/zio/ArmeriaZioServerOptions.scala index 801e9827af..2569a99dcc 100644 --- a/server/armeria-server/zio/src/main/scala/sttp/tapir/server/armeria/zio/ArmeriaZioServerOptions.scala +++ b/server/armeria-server/zio/src/main/scala/sttp/tapir/server/armeria/zio/ArmeriaZioServerOptions.scala @@ -1,6 +1,6 @@ package sttp.tapir.server.armeria.zio -import _root_.zio.{RIO, Task, URIO} +import _root_.zio.{RIO, ZIO} import com.linecorp.armeria.common.CommonPools import org.slf4j.{Logger, LoggerFactory} import sttp.tapir.server.armeria.ArmeriaServerOptions @@ -47,26 +47,26 @@ object ArmeriaZioServerOptions { doLogWhenReceived = debugLog(_, None), doLogWhenHandled = debugLog[R], doLogAllDecodeFailures = debugLog[R], - doLogExceptions = (msg: String, ex: Throwable) => URIO.succeed { logger.warn(msg, ex) }, - noLog = URIO.unit + doLogExceptions = (msg: String, ex: Throwable) => ZIO.succeed { logger.warn(msg, ex) }, + noLog = ZIO.unit ) private def debugLog[R](msg: String, exOpt: Option[Throwable]): RIO[R, Unit] = - URIO.succeed(exOpt match { + ZIO.succeed(exOpt match { case None => logger.debug(msg) case Some(ex) => logger.debug(msg, ex) }) private def blocking[R, T](body: => T): RIO[R, T] = { - Task.async { cb => + ZIO.async { cb => CommonPools .blockingTaskExecutor() .execute(() => { try { - cb(Task.succeed(body)) + cb(ZIO.succeed(body)) } catch { case NonFatal(ex) => - cb(Task.fail(ex)) + cb(ZIO.fail(ex)) } }) } diff --git a/server/armeria-server/zio/src/main/scala/sttp/tapir/server/armeria/zio/RIOMonadAsyncError.scala b/server/armeria-server/zio/src/main/scala/sttp/tapir/server/armeria/zio/RIOMonadAsyncError.scala index b77cecbabd..a44c2f7150 100644 --- a/server/armeria-server/zio/src/main/scala/sttp/tapir/server/armeria/zio/RIOMonadAsyncError.scala +++ b/server/armeria-server/zio/src/main/scala/sttp/tapir/server/armeria/zio/RIOMonadAsyncError.scala @@ -5,7 +5,7 @@ import zio._ // Forked from sttp.client3.impl.zio.RIOMonadAsyncError private class RIOMonadAsyncError[R] extends MonadAsyncError[RIO[R, *]] { - override def unit[T](t: T): RIO[R, T] = RIO.succeed(t) + override def unit[T](t: T): RIO[R, T] = ZIO.succeed(t) override def map[T, T2](fa: RIO[R, T])(f: T => T2): RIO[R, T2] = fa.map(f) @@ -13,23 +13,23 @@ private class RIOMonadAsyncError[R] extends MonadAsyncError[RIO[R, *]] { fa.flatMap(f) override def async[T](register: (Either[Throwable, T] => Unit) => Canceler): RIO[R, T] = - RIO.asyncInterrupt { cb => + ZIO.asyncInterrupt { cb => val canceler = register { - case Left(t) => cb(RIO.fail(t)) - case Right(t) => cb(RIO.succeed(t)) + case Left(t) => cb(ZIO.fail(t)) + case Right(t) => cb(ZIO.succeed(t)) } - Left(UIO.succeed(canceler.cancel())) + Left(ZIO.succeed(canceler.cancel())) } - override def error[T](t: Throwable): RIO[R, T] = RIO.fail(t) + override def error[T](t: Throwable): RIO[R, T] = ZIO.fail(t) override protected def handleWrappedError[T](rt: RIO[R, T])(h: PartialFunction[Throwable, RIO[R, T]]): RIO[R, T] = rt.catchSome(h) - override def eval[T](t: => T): RIO[R, T] = RIO.attempt(t) + override def eval[T](t: => T): RIO[R, T] = ZIO.attempt(t) - override def suspend[T](t: => RIO[R, T]): RIO[R, T] = RIO.suspend(t) + override def suspend[T](t: => RIO[R, T]): RIO[R, T] = ZIO.suspend(t) override def flatten[T](ffa: RIO[R, RIO[R, T]]): RIO[R, T] = ffa.flatten diff --git a/server/armeria-server/zio/src/main/scala/sttp/tapir/server/armeria/zio/TapirZioService.scala b/server/armeria-server/zio/src/main/scala/sttp/tapir/server/armeria/zio/TapirZioService.scala index 82a22ea13e..4dce003038 100644 --- a/server/armeria-server/zio/src/main/scala/sttp/tapir/server/armeria/zio/TapirZioService.scala +++ b/server/armeria-server/zio/src/main/scala/sttp/tapir/server/armeria/zio/TapirZioService.scala @@ -40,7 +40,7 @@ private[zio] final case class TapirZioService[R]( armeriaServerOptions.deleteFile ) - val serverRequest = new ArmeriaServerRequest(ctx) + val serverRequest = ArmeriaServerRequest(ctx) val future = new CompletableFuture[HttpResponse]() val result = interpreter(serverRequest).map(ResultMapping.toArmeria) @@ -73,17 +73,17 @@ private object ZioStreamCompatible { runtime.unsafeRun(stream.mapChunks(c => Chunk.single(HttpData.wrap(c.toArray))).toPublisher) override def fromArmeriaStream(publisher: Publisher[HttpData]): Stream[Throwable, Byte] = - publisher.toStream().mapConcatChunk(httpData => Chunk.fromArray(httpData.array())) + publisher.toZIOStream().mapConcatChunk(httpData => Chunk.fromArray(httpData.array())) } } } private class RioFutureConversion[R](implicit ec: ExecutionContext, runtime: Runtime[R]) extends FutureConversion[RIO[R, *]] { def from[T](f: => Future[T]): RIO[R, T] = { - RIO.async { cb => + ZIO.async { cb => f.onComplete { - case Failure(exception) => cb(Task.fail(exception)) - case Success(value) => cb(Task.succeed(value)) + case Failure(exception) => cb(ZIO.fail(exception)) + case Success(value) => cb(ZIO.succeed(value)) } } } diff --git a/server/finatra-server/src/main/scala/sttp/tapir/server/finatra/FinatraServerInterpreter.scala b/server/finatra-server/src/main/scala/sttp/tapir/server/finatra/FinatraServerInterpreter.scala index 366cc938a5..7847ca68ba 100644 --- a/server/finatra-server/src/main/scala/sttp/tapir/server/finatra/FinatraServerInterpreter.scala +++ b/server/finatra-server/src/main/scala/sttp/tapir/server/finatra/FinatraServerInterpreter.scala @@ -11,7 +11,7 @@ import sttp.tapir.server.ServerEndpoint import sttp.tapir.server.finatra.FinatraServerInterpreter.FutureMonadError import sttp.tapir.server.interceptor.RequestResult import sttp.tapir.server.interpreter.ServerInterpreter -import sttp.tapir.{AnyEndpoint, EndpointInput} +import sttp.tapir.{AnyEndpoint, EndpointInput, noTrailingSlash} trait FinatraServerInterpreter extends Logging { @@ -69,7 +69,14 @@ trait FinatraServerInterpreter extends Logging { case EndpointInput.PathsCapture(_, _) => "/:*" } .mkString - if (p.isEmpty) "/:*" else p + if (p.isEmpty) "/:*" + // checking if there's an input which rejects trailing slashes; otherwise the default behavior is to accept them + else if ( + input.traverseInputs { + case i if i == noTrailingSlash => Vector(()) + }.isEmpty + ) p + "/?" + else p } private[finatra] def httpMethod(endpoint: AnyEndpoint): Method = endpoint.method.map(m => Method(m.method)).getOrElse(Method("ANY")) diff --git a/server/finatra-server/src/main/scala/sttp/tapir/server/finatra/TapirController.scala b/server/finatra-server/src/main/scala/sttp/tapir/server/finatra/TapirController.scala index 84b49bfdf0..4f76baad19 100644 --- a/server/finatra-server/src/main/scala/sttp/tapir/server/finatra/TapirController.scala +++ b/server/finatra-server/src/main/scala/sttp/tapir/server/finatra/TapirController.scala @@ -1,36 +1,37 @@ package sttp.tapir.server.finatra import com.twitter.finagle.http.Method._ +import com.twitter.finagle.http.{Request, Response} import com.twitter.finatra.http.Controller +import com.twitter.util.Future trait TapirController { self: Controller => def addTapirRoute(route: FinatraRoute): Unit = { route.method match { case Get => - get(route.path + "/?")(route.handler) + get(route.path)(route.handler) case Post => - post(route.path + "/?")(route.handler) + post(route.path)(route.handler) case Put => - put(route.path + "/?")(route.handler) + put(route.path)(route.handler) case Head => - head(route.path + "/?")(route.handler) + head(route.path)(route.handler) case Patch => - patch(route.path + "/?")(route.handler) + patch(route.path)(route.handler) case Delete => - delete(route.path + "/?")(route.handler) + delete(route.path)(route.handler) case Trace => - // TODO - function name: trace conflicts with Logging's - any(route.path + "/?")(route.handler) + trace[Request, Future[Response]](route.path)(route.handler) case Options => - options(route.path + "/?")(route.handler) + options(route.path)(route.handler) case _ => - get(route.path + "/?")(route.handler) - post(route.path + "/?")(route.handler) - put(route.path + "/?")(route.handler) - head(route.path + "/?")(route.handler) - patch(route.path + "/?")(route.handler) - delete(route.path + "/?")(route.handler) - options(route.path + "/?")(route.handler) + get(route.path)(route.handler) + post(route.path)(route.handler) + put(route.path)(route.handler) + head(route.path)(route.handler) + patch(route.path)(route.handler) + delete(route.path)(route.handler) + options(route.path)(route.handler) } } } diff --git a/server/http4s-server/zio/src/test/scala/sttp/tapir/server/http4s/ztapir/ZHttp4sServerTest.scala b/server/http4s-server/zio/src/test/scala/sttp/tapir/server/http4s/ztapir/ZHttp4sServerTest.scala index 17569a9fc1..5b87297e9c 100644 --- a/server/http4s-server/zio/src/test/scala/sttp/tapir/server/http4s/ztapir/ZHttp4sServerTest.scala +++ b/server/http4s-server/zio/src/test/scala/sttp/tapir/server/http4s/ztapir/ZHttp4sServerTest.scala @@ -14,7 +14,8 @@ import sttp.tapir.server.http4s.Http4sServerSentEvents import sttp.tapir.server.tests._ import sttp.tapir.tests.{Test, TestSuite} import zio.interop.catz._ -import zio.{Task, UIO} +import zio.stream.ZStream +import zio.{Task, ZIO} import java.util.UUID import scala.util.Random @@ -35,7 +36,7 @@ class ZHttp4sServerTest extends TestSuite with OptionValues { createServerTest.testServer( endpoint.out(serverSentEventsBody), "Send and receive SSE" - )((_: Unit) => UIO.right(zio.stream.Stream(sse1, sse2))) { (backend, baseUri) => + )((_: Unit) => ZIO.right(ZStream(sse1, sse2))) { (backend, baseUri) => basicRequest .response(asStream[IO, List[ServerSentEvent], Fs2Streams[IO]](Fs2Streams[IO]) { stream => Http4sServerSentEvents @@ -54,7 +55,7 @@ class ZHttp4sServerTest extends TestSuite with OptionValues { new ServerStreamingTests(createServerTest, ZioStreams).tests() ++ new ServerWebSocketTests(createServerTest, ZioStreams) { override def functionToPipe[A, B](f: A => B): streams.Pipe[A, B] = in => in.map(f) - override def emptyPipe[A, B]: streams.Pipe[A, B] = _ => zio.stream.Stream.empty + override def emptyPipe[A, B]: streams.Pipe[A, B] = _ => ZStream.empty }.tests() ++ additionalTests() } diff --git a/server/tests/src/main/scala/sttp/tapir/server/tests/ServerBasicTests.scala b/server/tests/src/main/scala/sttp/tapir/server/tests/ServerBasicTests.scala index 0d51fd58af..0ffbdc48ca 100644 --- a/server/tests/src/main/scala/sttp/tapir/server/tests/ServerBasicTests.scala +++ b/server/tests/src/main/scala/sttp/tapir/server/tests/ServerBasicTests.scala @@ -579,6 +579,17 @@ class ServerBasicTests[F[_], OPTIONS, ROUTE]( r.code shouldBe StatusCode.Ok r.body shouldBe Right("ok1") } + }, + testServer( + "two endpoints, same path prefix, one without trailing slashes, second accepting trailing slashes", + NonEmptyList.of( + route(endpoint.get.in("p1" / "p2").in(noTrailingSlash).out(stringBody).serverLogic((_: Unit) => pureResult("e1".asRight[Unit]))), + route(endpoint.get.in("p1" / "p2").in(paths).out(stringBody).serverLogic((_: List[String]) => pureResult("e2".asRight[Unit]))) + ) + ) { (backend, baseUri) => + basicStringRequest.get(uri"$baseUri/p1/p2").send(backend).map(_.body shouldBe "e1") >> + basicStringRequest.get(uri"$baseUri/p1/p2/").send(backend).map(_.body shouldBe "e2") >> + basicStringRequest.get(uri"$baseUri/p1/p2/p3").send(backend).map(_.body shouldBe "e2") } ) diff --git a/server/vertx-server/cats/src/main/scala/sttp/tapir/server/vertx/cats/VertxCatsServerInterpreter.scala b/server/vertx-server/cats/src/main/scala/sttp/tapir/server/vertx/cats/VertxCatsServerInterpreter.scala index 987c9fd3d3..2b791d28ea 100644 --- a/server/vertx-server/cats/src/main/scala/sttp/tapir/server/vertx/cats/VertxCatsServerInterpreter.scala +++ b/server/vertx-server/cats/src/main/scala/sttp/tapir/server/vertx/cats/VertxCatsServerInterpreter.scala @@ -62,7 +62,8 @@ trait VertxCatsServerInterpreter[F[_]] extends CommonServerInterpreter { val result = interpreter(serverRequest) .flatMap { - case RequestResult.Failure(_) => fFromVFuture(rc.response.setStatusCode(404).end()).void + // in vertx, endpoints are attempted to be decoded individually; if this endpoint didn't match - another one might + case RequestResult.Failure(_) => Async[F].delay(rc.next()) case RequestResult.Response(response) => fFromVFuture(VertxOutputEncoders(response).apply(rc)).void } .handleError { ex => diff --git a/server/vertx-server/src/main/scala/sttp/tapir/server/vertx/VertxFutureServerInterpreter.scala b/server/vertx-server/src/main/scala/sttp/tapir/server/vertx/VertxFutureServerInterpreter.scala index 1427cdb135..1f7ad50984 100644 --- a/server/vertx-server/src/main/scala/sttp/tapir/server/vertx/VertxFutureServerInterpreter.scala +++ b/server/vertx-server/src/main/scala/sttp/tapir/server/vertx/VertxFutureServerInterpreter.scala @@ -62,7 +62,8 @@ trait VertxFutureServerInterpreter extends CommonServerInterpreter { interpreter(serverRequest) .flatMap { - case RequestResult.Failure(_) => FutureFromVFuture(rc.response.setStatusCode(404).end()) + // in vertx, endpoints are attempted to be decoded individually; if this endpoint didn't match - another one might + case RequestResult.Failure(_) => Future.successful(rc.next()) case RequestResult.Response(response) => FutureFromVFuture(VertxOutputEncoders(response).apply(rc)) } .failed diff --git a/server/vertx-server/zio/src/main/scala/sttp/tapir/server/vertx/zio/VertxZioServerInterpreter.scala b/server/vertx-server/zio/src/main/scala/sttp/tapir/server/vertx/zio/VertxZioServerInterpreter.scala index ef9c058804..8a1403e985 100644 --- a/server/vertx-server/zio/src/main/scala/sttp/tapir/server/vertx/zio/VertxZioServerInterpreter.scala +++ b/server/vertx-server/zio/src/main/scala/sttp/tapir/server/vertx/zio/VertxZioServerInterpreter.scala @@ -4,7 +4,6 @@ import io.vertx.core.logging.LoggerFactory import io.vertx.core.{Future, Handler, Promise} import io.vertx.ext.web.{Route, Router, RoutingContext} import sttp.capabilities.zio.ZioStreams -import sttp.monad.MonadError import sttp.tapir.server.interceptor.RequestResult import sttp.tapir.server.interpreter.{BodyListener, ServerInterpreter} import sttp.tapir.server.vertx.VertxBodyListener @@ -12,9 +11,9 @@ import sttp.tapir.server.vertx.decoders.{VertxRequestBody, VertxServerRequest} import sttp.tapir.server.vertx.encoders.{VertxOutputEncoders, VertxToResponseBody} import sttp.tapir.server.vertx.interpreters.{CommonServerInterpreter, FromVFuture} import sttp.tapir.server.vertx.routing.PathMapping.extractRouteDefinition -import sttp.tapir.server.vertx.zio.VertxZioServerInterpreter.{RioFromVFuture, monadError} +import sttp.tapir.server.vertx.zio.VertxZioServerInterpreter.RioFromVFuture import sttp.tapir.server.vertx.zio.streams._ -import sttp.tapir.ztapir.ZServerEndpoint +import sttp.tapir.ztapir.{RIOMonadError, ZServerEndpoint} import zio._ import java.util.concurrent.atomic.AtomicReference @@ -36,6 +35,7 @@ trait VertxZioServerInterpreter[R] extends CommonServerInterpreter { e: ZServerEndpoint[R, ZioStreams] )(implicit runtime: Runtime[R]): Handler[RoutingContext] = { val fromVFuture = new RioFromVFuture[R] + implicit val monadError: RIOMonadError[R] = new RIOMonadError[R] implicit val bodyListener: BodyListener[RIO[R, *], RoutingContext => Future[Void]] = new VertxBodyListener[RIO[R, *]] val zioReadStream = zioReadStreamCompatible(vertxZioServerOptions) val interpreter = new ServerInterpreter[ZioStreams, RIO[R, *], RoutingContext => Future[Void], ZioStreams]( @@ -52,18 +52,19 @@ trait VertxZioServerInterpreter[R] extends CommonServerInterpreter { val result: ZIO[R, Throwable, Any] = interpreter(serverRequest) .flatMap { - case RequestResult.Failure(decodeFailureContexts) => fromVFuture(rc.response.setStatusCode(404).end()) + // in vertx, endpoints are attempted to be decoded individually; if this endpoint didn't match - another one might + case RequestResult.Failure(_) => ZIO.succeed(rc.next()) case RequestResult.Response(response) => - Task.async((k: Task[Unit] => Unit) => { + ZIO.async((k: Task[Unit] => Unit) => { VertxOutputEncoders(response) .apply(rc) .onComplete(d => { - if (d.succeeded()) k(Task.unit) else k(Task.fail(d.cause())) + if (d.succeeded()) k(ZIO.unit) else k(ZIO.fail(d.cause())) }) }) } .catchAll { ex => - RIO.attempt({ + ZIO.attempt({ logger.error("Error while processing the request", ex) if (rc.response().bytesWritten() > 0) rc.response().end() rc.fail(ex) @@ -113,30 +114,18 @@ object VertxZioServerInterpreter { } } - private[vertx] implicit def monadError[R]: MonadError[RIO[R, *]] = new MonadError[RIO[R, *]] { - override def unit[T](t: T): RIO[R, T] = Task.succeed(t) - override def map[T, T2](fa: RIO[R, T])(f: T => T2): RIO[R, T2] = fa.map(f) - override def flatMap[T, T2](fa: RIO[R, T])(f: T => RIO[R, T2]): RIO[R, T2] = fa.flatMap(f) - override def error[T](t: Throwable): RIO[R, T] = Task.fail(t) - override protected def handleWrappedError[T](rt: RIO[R, T])(h: PartialFunction[Throwable, RIO[R, T]]): RIO[R, T] = rt.catchSome(h) - override def eval[T](t: => T): RIO[R, T] = Task.attempt(t) - override def suspend[T](t: => RIO[R, T]): RIO[R, T] = RIO.suspend(t) - override def flatten[T](ffa: RIO[R, RIO[R, T]]): RIO[R, T] = ffa.flatten - override def ensure[T](f: RIO[R, T], e: => RIO[R, Unit]): RIO[R, T] = f.ensuring(e.ignore) - } - private[vertx] class RioFromVFuture[R] extends FromVFuture[RIO[R, *]] { def apply[T](f: => Future[T]): RIO[R, T] = f.asRIO } implicit class VertxFutureToRIO[A](f: => Future[A]) { def asRIO[R]: RIO[R, A] = { - RIO.async { cb => + ZIO.async { cb => f.onComplete { handler => if (handler.succeeded()) { - cb(Task.succeed(handler.result())) + cb(ZIO.succeed(handler.result())) } else { - cb(Task.fail(handler.cause())) + cb(ZIO.fail(handler.cause())) } } } diff --git a/server/vertx-server/zio/src/main/scala/sttp/tapir/server/vertx/zio/VertxZioServerOptions.scala b/server/vertx-server/zio/src/main/scala/sttp/tapir/server/vertx/zio/VertxZioServerOptions.scala index d433b9dc69..0834e3a9d3 100644 --- a/server/vertx-server/zio/src/main/scala/sttp/tapir/server/vertx/zio/VertxZioServerOptions.scala +++ b/server/vertx-server/zio/src/main/scala/sttp/tapir/server/vertx/zio/VertxZioServerOptions.scala @@ -5,7 +5,7 @@ import sttp.tapir.server.interceptor.log.{DefaultServerLog, ServerLog} import sttp.tapir.server.interceptor.{CustomiseInterceptors, Interceptor} import sttp.tapir.server.vertx.VertxServerOptions import sttp.tapir.{Defaults, TapirFile} -import zio.{RIO, Task, URIO} +import zio.{RIO, ZIO} final case class VertxZioServerOptions[F[_]]( uploadDirectory: TapirFile, @@ -27,7 +27,7 @@ object VertxZioServerOptions { createOptions = (ci: CustomiseInterceptors[RIO[R, *], VertxZioServerOptions[RIO[R, *]]]) => VertxZioServerOptions( VertxServerOptions.uploadDirectory(), - file => Task.attemptBlocking(Defaults.deleteFile()(file)), + file => ZIO.attemptBlocking(Defaults.deleteFile()(file)), maxQueueSizeForReadStream = 16, ci.interceptors ) @@ -40,16 +40,16 @@ object VertxZioServerOptions { doLogWhenReceived = debugLog(log)(_, None), doLogWhenHandled = debugLog(log), doLogAllDecodeFailures = infoLog(log), - doLogExceptions = (msg: String, ex: Throwable) => URIO.succeed { log.error(msg, ex) }, - noLog = URIO.unit + doLogExceptions = (msg: String, ex: Throwable) => ZIO.succeed { log.error(msg, ex) }, + noLog = ZIO.unit ) } - private def debugLog[R](log: Logger)(msg: String, exOpt: Option[Throwable]): RIO[R, Unit] = URIO.succeed { + private def debugLog[R](log: Logger)(msg: String, exOpt: Option[Throwable]): RIO[R, Unit] = ZIO.succeed { VertxServerOptions.debugLog(log)(msg, exOpt) } - private def infoLog[R](log: Logger)(msg: String, exOpt: Option[Throwable]): RIO[R, Unit] = URIO.succeed { + private def infoLog[R](log: Logger)(msg: String, exOpt: Option[Throwable]): RIO[R, Unit] = ZIO.succeed { VertxServerOptions.infoLog(log)(msg, exOpt) } } diff --git a/server/vertx-server/zio/src/main/scala/sttp/tapir/server/vertx/zio/streams/zio.scala b/server/vertx-server/zio/src/main/scala/sttp/tapir/server/vertx/zio/streams/zio.scala index c580c2cf15..2b65019215 100644 --- a/server/vertx-server/zio/src/main/scala/sttp/tapir/server/vertx/zio/streams/zio.scala +++ b/server/vertx-server/zio/src/main/scala/sttp/tapir/server/vertx/zio/streams/zio.scala @@ -96,7 +96,7 @@ package object streams { runtime .unsafeRunSync(for { oldState <- state.getAndUpdate(_.copy(paused = None)) - _ <- oldState.paused.fold[UIO[Any]](UIO.unit)(_.complete(())) + _ <- oldState.paused.fold[UIO[Any]](ZIO.unit)(_.complete(())) } yield self) .toEither .fold(throw _, identity) @@ -122,12 +122,12 @@ package object streams { case Left(deferred) => deferred.get case Right(buffer) => - UIO.succeed(buffer) + ZIO.succeed(buffer) } result <- wrappedBuffer match { - case Right(buffer) => UIO.some((buffer, ())) - case Left(None) => UIO.none - case Left(Some(cause)) => IO.fail(cause) + case Right(buffer) => ZIO.some((buffer, ())) + case Left(None) => ZIO.none + case Left(Some(cause)) => ZIO.fail(cause) } } yield result } @@ -140,15 +140,15 @@ package object streams { case Left(deferred) => deferred.get case Right(event) => - UIO.succeed(event) + ZIO.succeed(event) } } yield result.map((_, ())) }) .mapZIO({ case Pause => - IO.attempt(readStream.pause()) + ZIO.attempt(readStream.pause()) case Resume => - IO.attempt(readStream.resume()) + ZIO.attempt(readStream.resume()) }) .runDrain .forkDaemon diff --git a/server/vertx-server/zio/src/test/scala/sttp/tapir/server/vertx/zio/VertxStubServerTest.scala b/server/vertx-server/zio/src/test/scala/sttp/tapir/server/vertx/zio/VertxStubServerTest.scala index 577688bfc6..96bdc2b64e 100644 --- a/server/vertx-server/zio/src/test/scala/sttp/tapir/server/vertx/zio/VertxStubServerTest.scala +++ b/server/vertx-server/zio/src/test/scala/sttp/tapir/server/vertx/zio/VertxStubServerTest.scala @@ -6,13 +6,13 @@ import sttp.tapir.server.interceptor.CustomiseInterceptors import sttp.tapir.server.tests.{CreateServerStubTest, ServerStubStreamingTest, ServerStubTest} import _root_.zio.stream.ZStream import _root_.zio.{Runtime, Task} -import sttp.tapir.server.vertx.zio.{VertxZioServerInterpreter, VertxZioServerOptions} +import sttp.tapir.ztapir.RIOMonadError import scala.concurrent.Future object VertxZioCreateServerStubTest extends CreateServerStubTest[Task, VertxZioServerOptions[Task]] { override def customiseInterceptors: CustomiseInterceptors[Task, VertxZioServerOptions[Task]] = VertxZioServerOptions.customiseInterceptors - override def stub[R]: SttpBackendStub[Task, R] = SttpBackendStub(VertxZioServerInterpreter.monadError) + override def stub[R]: SttpBackendStub[Task, R] = SttpBackendStub(new RIOMonadError[Any]) override def asFuture[A]: Task[A] => Future[A] = task => Runtime.default.unsafeRunToFuture(task) } diff --git a/server/vertx-server/zio/src/test/scala/sttp/tapir/server/vertx/zio/ZioVertxServerTest.scala b/server/vertx-server/zio/src/test/scala/sttp/tapir/server/vertx/zio/ZioVertxServerTest.scala index 46e5261743..aa06055378 100644 --- a/server/vertx-server/zio/src/test/scala/sttp/tapir/server/vertx/zio/ZioVertxServerTest.scala +++ b/server/vertx-server/zio/src/test/scala/sttp/tapir/server/vertx/zio/ZioVertxServerTest.scala @@ -8,6 +8,7 @@ import sttp.monad.MonadError import sttp.tapir.server.tests._ import sttp.tapir.tests.{Test, TestSuite} import _root_.zio.Task +import sttp.tapir.ztapir.RIOMonadError class ZioVertxServerTest extends TestSuite { def vertxResource: Resource[IO, Vertx] = @@ -15,7 +16,7 @@ class ZioVertxServerTest extends TestSuite { override def tests: Resource[IO, List[Test]] = backendResource.flatMap { backend => vertxResource.map { implicit vertx => - implicit val m: MonadError[Task] = VertxZioServerInterpreter.monadError + implicit val m: MonadError[Task] = new RIOMonadError[Any] val interpreter = new ZioVertxTestServerInterpreter(vertx) val createServerTest = new DefaultCreateServerTest(backend, interpreter) diff --git a/server/vertx-server/zio/src/test/scala/sttp/tapir/server/vertx/zio/streams/ZStreamTest.scala b/server/vertx-server/zio/src/test/scala/sttp/tapir/server/vertx/zio/streams/ZStreamTest.scala index 34187d4335..4bf83aa086 100644 --- a/server/vertx-server/zio/src/test/scala/sttp/tapir/server/vertx/zio/streams/ZStreamTest.scala +++ b/server/vertx-server/zio/src/test/scala/sttp/tapir/server/vertx/zio/streams/ZStreamTest.scala @@ -62,24 +62,24 @@ class ZStreamTest extends AsyncFlatSpec with Matchers { .unsafeRunToFuture(for { ref <- Ref.make[List[Int]](Nil) completed <- Ref.make[Boolean](false) - _ <- Task.attempt { + _ <- ZIO.attempt { readStream.handler { buffer => runtime.unsafeRunSync(ref.update(_ :+ bufferAsInt(buffer))) () } } - _ <- Task.attempt { + _ <- ZIO.attempt { readStream.endHandler { _ => runtime.unsafeRunSync(completed.set(true)) () } } - _ <- Task.attempt(readStream.resume()) + _ <- ZIO.attempt(readStream.resume()) _ <- eventually(ref.get)({ case _ :: _ => () }) - _ <- Task.attempt(readStream.pause()) + _ <- ZIO.attempt(readStream.pause()) _ <- ZIO.sleep(1.seconds) snapshot2 <- ref.get - _ <- Task.attempt(readStream.resume()) + _ <- ZIO.attempt(readStream.resume()) snapshot3 <- eventually(ref.get)({ case list => list.length should be > snapshot2.length }) _ = shouldIncreaseMonotonously(snapshot3) _ <- eventually(completed.get)({ case true => () }) @@ -100,25 +100,25 @@ class ZStreamTest extends AsyncFlatSpec with Matchers { ref <- Ref.make[List[Int]](Nil) completedRef <- Ref.make[Boolean](false) interruptedRef <- Ref.make[Option[Throwable]](None) - _ <- Task.attempt { + _ <- ZIO.attempt { readStream.handler { buffer => runtime.unsafeRunSync(ref.update(_ :+ bufferAsInt(buffer))) () } } - _ <- Task.attempt { + _ <- ZIO.attempt { readStream.endHandler { _ => runtime.unsafeRunSync(completedRef.set(true)) () } } - _ <- Task.attempt { + _ <- ZIO.attempt { readStream.exceptionHandler { cause => runtime.unsafeRunSync(interruptedRef.set(Some(cause))) () } } - _ <- Task.attempt(readStream.resume()) + _ <- ZIO.attempt(readStream.resume()) snapshot <- eventually(ref.get)({ case list => list.length should be > 3 }) _ = shouldIncreaseMonotonously(snapshot) _ <- eventually(completedRef.get zip interruptedRef.get)({ case (false, Some(_)) => diff --git a/server/vertx-server/zio1/src/main/scala/sttp/tapir/server/vertx/zio/VertxZioServerInterpreter.scala b/server/vertx-server/zio1/src/main/scala/sttp/tapir/server/vertx/zio/VertxZioServerInterpreter.scala index 1394bfb6cd..50cfa31de9 100644 --- a/server/vertx-server/zio1/src/main/scala/sttp/tapir/server/vertx/zio/VertxZioServerInterpreter.scala +++ b/server/vertx-server/zio1/src/main/scala/sttp/tapir/server/vertx/zio/VertxZioServerInterpreter.scala @@ -4,17 +4,16 @@ import io.vertx.core.logging.LoggerFactory import io.vertx.core.{Future, Handler, Promise} import io.vertx.ext.web.{Route, Router, RoutingContext} import sttp.capabilities.zio.ZioStreams -import sttp.monad.MonadError import sttp.tapir.server.interceptor.RequestResult import sttp.tapir.server.interpreter.{BodyListener, ServerInterpreter} -import sttp.tapir.server.vertx.zio.VertxZioServerInterpreter.{RioFromVFuture, monadError} +import sttp.tapir.server.vertx.zio.VertxZioServerInterpreter.RioFromVFuture import sttp.tapir.server.vertx.decoders.{VertxRequestBody, VertxServerRequest} import sttp.tapir.server.vertx.encoders.{VertxOutputEncoders, VertxToResponseBody} import sttp.tapir.server.vertx.interpreters.{CommonServerInterpreter, FromVFuture} import sttp.tapir.server.vertx.routing.PathMapping.extractRouteDefinition import sttp.tapir.server.vertx.zio.streams._ import sttp.tapir.server.vertx.VertxBodyListener -import sttp.tapir.ztapir.ZServerEndpoint +import sttp.tapir.ztapir.{RIOMonadError, ZServerEndpoint} import _root_.zio._ import _root_.zio.blocking.Blocking @@ -37,6 +36,7 @@ trait VertxZioServerInterpreter[R <: Blocking] extends CommonServerInterpreter { e: ZServerEndpoint[R, ZioStreams] )(implicit runtime: Runtime[R]): Handler[RoutingContext] = { val fromVFuture = new RioFromVFuture[R] + implicit val monadError: RIOMonadError[R] = new RIOMonadError[R] implicit val bodyListener: BodyListener[RIO[R, *], RoutingContext => Future[Void]] = new VertxBodyListener[RIO[R, *]] val zioReadStream = zioReadStreamCompatible(vertxZioServerOptions) val interpreter = new ServerInterpreter[ZioStreams, RIO[R, *], RoutingContext => Future[Void], ZioStreams]( @@ -53,7 +53,8 @@ trait VertxZioServerInterpreter[R <: Blocking] extends CommonServerInterpreter { val result: ZIO[R, Throwable, Any] = interpreter(serverRequest) .flatMap { - case RequestResult.Failure(decodeFailureContexts) => fromVFuture(rc.response.setStatusCode(404).end()) + // in vertx, endpoints are attempted to be decoded individually; if this endpoint didn't match - another one might + case RequestResult.Failure(_) => ZIO.succeed(rc.next()) case RequestResult.Response(response) => Task.effectAsync((k: Task[Unit] => Unit) => { VertxOutputEncoders(response) @@ -116,18 +117,6 @@ object VertxZioServerInterpreter { } } - private[vertx] implicit def monadError[R]: MonadError[RIO[R, *]] = new MonadError[RIO[R, *]] { - override def unit[T](t: T): RIO[R, T] = Task.succeed(t) - override def map[T, T2](fa: RIO[R, T])(f: T => T2): RIO[R, T2] = fa.map(f) - override def flatMap[T, T2](fa: RIO[R, T])(f: T => RIO[R, T2]): RIO[R, T2] = fa.flatMap(f) - override def error[T](t: Throwable): RIO[R, T] = Task.fail(t) - override protected def handleWrappedError[T](rt: RIO[R, T])(h: PartialFunction[Throwable, RIO[R, T]]): RIO[R, T] = rt.catchSome(h) - override def eval[T](t: => T): RIO[R, T] = Task.effect(t) - override def suspend[T](t: => RIO[R, T]): RIO[R, T] = RIO.effectSuspend(t) - override def flatten[T](ffa: RIO[R, RIO[R, T]]): RIO[R, T] = ffa.flatten - override def ensure[T](f: RIO[R, T], e: => RIO[R, Unit]): RIO[R, T] = f.ensuring(e.ignore) - } - private[vertx] class RioFromVFuture[R] extends FromVFuture[RIO[R, *]] { def apply[T](f: => Future[T]): RIO[R, T] = f.asRIO } diff --git a/server/vertx-server/zio1/src/test/scala/sttp/tapir/server/vertx/zio/VertxStubServerTest.scala b/server/vertx-server/zio1/src/test/scala/sttp/tapir/server/vertx/zio/VertxStubServerTest.scala index 450e60d6c7..2334285bc2 100644 --- a/server/vertx-server/zio1/src/test/scala/sttp/tapir/server/vertx/zio/VertxStubServerTest.scala +++ b/server/vertx-server/zio1/src/test/scala/sttp/tapir/server/vertx/zio/VertxStubServerTest.scala @@ -5,15 +5,16 @@ import sttp.client3.testing.SttpBackendStub import sttp.tapir.server.interceptor.CustomiseInterceptors import sttp.tapir.server.tests.{CreateServerStubTest, ServerStubStreamingTest, ServerStubTest} import _root_.zio.stream.ZStream -import _root_.zio.{Runtime, RIO} +import _root_.zio.{RIO, Runtime} import _root_.zio.blocking.Blocking +import sttp.tapir.ztapir.RIOMonadError import scala.concurrent.Future object VertxZioCreateServerStubTest extends CreateServerStubTest[RIO[Blocking, *], VertxZioServerOptions[RIO[Blocking, *]]] { override def customiseInterceptors: CustomiseInterceptors[RIO[Blocking, *], VertxZioServerOptions[RIO[Blocking, *]]] = VertxZioServerOptions.customiseInterceptors - override def stub[R]: SttpBackendStub[RIO[Blocking, *], R] = SttpBackendStub(VertxZioServerInterpreter.monadError) + override def stub[R]: SttpBackendStub[RIO[Blocking, *], R] = SttpBackendStub(new RIOMonadError[Blocking]) override def asFuture[A]: RIO[Blocking, A] => Future[A] = task => Runtime.default.unsafeRunToFuture(task) } diff --git a/server/vertx-server/zio1/src/test/scala/sttp/tapir/server/vertx/zio/ZioVertxServerTest.scala b/server/vertx-server/zio1/src/test/scala/sttp/tapir/server/vertx/zio/ZioVertxServerTest.scala index efe65a6c30..9f59f0f0e5 100644 --- a/server/vertx-server/zio1/src/test/scala/sttp/tapir/server/vertx/zio/ZioVertxServerTest.scala +++ b/server/vertx-server/zio1/src/test/scala/sttp/tapir/server/vertx/zio/ZioVertxServerTest.scala @@ -9,6 +9,7 @@ import sttp.tapir.server.tests._ import sttp.tapir.tests.{Test, TestSuite} import _root_.zio.RIO import _root_.zio.blocking.Blocking +import sttp.tapir.ztapir.RIOMonadError class ZioVertxServerTest extends TestSuite { def vertxResource: Resource[IO, Vertx] = @@ -16,7 +17,7 @@ class ZioVertxServerTest extends TestSuite { override def tests: Resource[IO, List[Test]] = backendResource.flatMap { backend => vertxResource.map { implicit vertx => - implicit val m: MonadError[RIO[Blocking, *]] = VertxZioServerInterpreter.monadError + implicit val m: MonadError[RIO[Blocking, *]] = new RIOMonadError[Blocking] val interpreter = new ZioVertxTestServerInterpreter(vertx) val createServerTest = new DefaultCreateServerTest(backend, interpreter) diff --git a/server/zio-http-server/src/main/scala/sttp/tapir/server/ziohttp/ZioHttpBodyListener.scala b/server/zio-http-server/src/main/scala/sttp/tapir/server/ziohttp/ZioHttpBodyListener.scala index deb45057c3..d81a72de0e 100644 --- a/server/zio-http-server/src/main/scala/sttp/tapir/server/ziohttp/ZioHttpBodyListener.scala +++ b/server/zio-http-server/src/main/scala/sttp/tapir/server/ziohttp/ZioHttpBodyListener.scala @@ -1,14 +1,14 @@ package sttp.tapir.server.ziohttp import sttp.tapir.server.interpreter.BodyListener -import zio.RIO +import zio.{RIO, ZIO} import zio.stream.ZStream import scala.util.{Failure, Success, Try} class ZioHttpBodyListener[R] extends BodyListener[RIO[R, *], ZioHttpResponseBody] { override def onComplete(body: ZioHttpResponseBody)(cb: Try[Unit] => RIO[R, Unit]): RIO[R, ZioHttpResponseBody] = - RIO + ZIO .environmentWith[R] .apply { r => val (stream, contentLength) = body diff --git a/server/zio-http-server/src/main/scala/sttp/tapir/server/ziohttp/ZioHttpInterpreter.scala b/server/zio-http-server/src/main/scala/sttp/tapir/server/ziohttp/ZioHttpInterpreter.scala index 99678fc2c0..970d0c79f9 100644 --- a/server/zio-http-server/src/main/scala/sttp/tapir/server/ziohttp/ZioHttpInterpreter.scala +++ b/server/zio-http-server/src/main/scala/sttp/tapir/server/ziohttp/ZioHttpInterpreter.scala @@ -29,7 +29,7 @@ trait ZioHttpInterpreter[R] { zioHttpServerOptions.deleteFile ) - Http.route[Request] { case req => + Http.collectHttp[Request] { case req => Http.fromZIO { interpreter .apply(ZioHttpServerRequest(req)) diff --git a/server/zio-http-server/src/main/scala/sttp/tapir/server/ziohttp/ZioHttpRequestBody.scala b/server/zio-http-server/src/main/scala/sttp/tapir/server/ziohttp/ZioHttpRequestBody.scala index ffded68040..42e762122e 100644 --- a/server/zio-http-server/src/main/scala/sttp/tapir/server/ziohttp/ZioHttpRequestBody.scala +++ b/server/zio-http-server/src/main/scala/sttp/tapir/server/ziohttp/ZioHttpRequestBody.scala @@ -8,8 +8,7 @@ import sttp.tapir.model.ServerRequest import sttp.tapir.server.interpreter.RawValue import sttp.tapir.server.interpreter.RequestBody import zhttp.http.Request -import zio.RIO -import zio.Task +import zio.{RIO, Task, ZIO} import zio.stream.Stream import zio.stream.ZStream @@ -25,8 +24,8 @@ class ZioHttpRequestBody[R](serverOptions: ZioHttpServerOptions[R]) extends Requ case RawBodyType.ByteBufferBody => asByteArray(serverRequest).map(bytes => ByteBuffer.wrap(bytes)).map(RawValue(_)) case RawBodyType.InputStreamBody => asByteArray(serverRequest).map(new ByteArrayInputStream(_)).map(RawValue(_)) case RawBodyType.FileBody => - serverOptions.createFile(serverRequest).map(d => FileRange(d)).flatMap(file => Task.succeed(RawValue(file, Seq(file)))) - case RawBodyType.MultipartBody(_, _) => Task.never + serverOptions.createFile(serverRequest).map(d => FileRange(d)).flatMap(file => ZIO.succeed(RawValue(file, Seq(file)))) + case RawBodyType.MultipartBody(_, _) => ZIO.never } override def toStream(serverRequest: ServerRequest): streams.BinaryStream = stream(serverRequest).asInstanceOf[streams.BinaryStream] diff --git a/server/zio-http-server/src/main/scala/sttp/tapir/server/ziohttp/ZioHttpServerOptions.scala b/server/zio-http-server/src/main/scala/sttp/tapir/server/ziohttp/ZioHttpServerOptions.scala index a6d7456b3c..185355fe9c 100644 --- a/server/zio-http-server/src/main/scala/sttp/tapir/server/ziohttp/ZioHttpServerOptions.scala +++ b/server/zio-http-server/src/main/scala/sttp/tapir/server/ziohttp/ZioHttpServerOptions.scala @@ -3,7 +3,7 @@ package sttp.tapir.server.ziohttp import sttp.tapir.model.ServerRequest import sttp.tapir.server.interceptor.{CustomiseInterceptors, Interceptor} import sttp.tapir.{Defaults, TapirFile} -import zio.{RIO, Task} +import zio.{RIO, Task, ZIO} case class ZioHttpServerOptions[R]( createFile: ServerRequest => Task[TapirFile], @@ -29,9 +29,9 @@ object ZioHttpServerOptions { ) ) - def defaultCreateFile: ServerRequest => Task[TapirFile] = _ => Task.attempt(Defaults.createTempFile()) + def defaultCreateFile: ServerRequest => Task[TapirFile] = _ => ZIO.attempt(Defaults.createTempFile()) - def defaultDeleteFile[R]: TapirFile => Task[Unit] = file => Task.attempt(Defaults.deleteFile()(file)) + def defaultDeleteFile[R]: TapirFile => Task[Unit] = file => ZIO.attempt(Defaults.deleteFile()(file)) def default[R]: ZioHttpServerOptions[R] = customiseInterceptors.options } diff --git a/server/zio-http-server/src/main/scala/sttp/tapir/server/ziohttp/ZioHttpToResponseBody.scala b/server/zio-http-server/src/main/scala/sttp/tapir/server/ziohttp/ZioHttpToResponseBody.scala index 0c4ad341bf..933b60fbd5 100644 --- a/server/zio-http-server/src/main/scala/sttp/tapir/server/ziohttp/ZioHttpToResponseBody.scala +++ b/server/zio-http-server/src/main/scala/sttp/tapir/server/ziohttp/ZioHttpToResponseBody.scala @@ -5,7 +5,7 @@ import sttp.model.HasHeaders import sttp.tapir.server.interpreter.ToResponseBody import sttp.tapir.{CodecFormat, FileRange, RawBodyType, WebSocketBodyOutput} import zio.Chunk -import zio.stream.{Stream, ZStream} +import zio.stream.ZStream import java.nio.charset.Charset @@ -26,15 +26,15 @@ class ZioHttpToResponseBody extends ToResponseBody[ZioHttpResponseBody, ZioStrea pipe: streams.Pipe[REQ, RESP], o: WebSocketBodyOutput[streams.Pipe[REQ, RESP], REQ, RESP, _, ZioStreams] ): ZioHttpResponseBody = - (Stream.empty, None) // TODO + (ZStream.empty, None) // TODO private def rawValueToEntity[R](bodyType: RawBodyType[R], r: R): ZioHttpResponseBody = { bodyType match { case RawBodyType.StringBody(charset) => val bytes = r.toString.getBytes(charset) (ZStream.fromIterable(bytes), Some(bytes.length.toLong)) - case RawBodyType.ByteArrayBody => (Stream.fromChunk(Chunk.fromArray(r)), Some((r: Array[Byte]).length.toLong)) - case RawBodyType.ByteBufferBody => (Stream.fromChunk(Chunk.fromByteBuffer(r)), None) + case RawBodyType.ByteArrayBody => (ZStream.fromChunk(Chunk.fromArray(r)), Some((r: Array[Byte]).length.toLong)) + case RawBodyType.ByteBufferBody => (ZStream.fromChunk(Chunk.fromByteBuffer(r)), None) case RawBodyType.InputStreamBody => (ZStream.fromInputStream(r), None) case RawBodyType.FileBody => val tapirFile = r.asInstanceOf[FileRange] @@ -53,7 +53,7 @@ class ZioHttpToResponseBody extends ToResponseBody[ZioHttpResponseBody, ZioStrea } .getOrElse(ZStream.fromPath(tapirFile.file.toPath)) (stream, Some(tapirFile.file.length)) - case RawBodyType.MultipartBody(_, _) => (Stream.empty, None) + case RawBodyType.MultipartBody(_, _) => (ZStream.empty, None) } } } diff --git a/server/zio-http-server/src/test/scala/sttp/tapir/server/ziohttp/ZioHttpCompositionTest.scala b/server/zio-http-server/src/test/scala/sttp/tapir/server/ziohttp/ZioHttpCompositionTest.scala index acc26c06b4..98a7c57b03 100644 --- a/server/zio-http-server/src/test/scala/sttp/tapir/server/ziohttp/ZioHttpCompositionTest.scala +++ b/server/zio-http-server/src/test/scala/sttp/tapir/server/ziohttp/ZioHttpCompositionTest.scala @@ -1,13 +1,14 @@ package sttp.tapir.server.ziohttp import cats.data.NonEmptyList +import org.scalactic.source.Position.here +import org.scalatest.matchers.should.Matchers._ import sttp.client3._ import sttp.model.StatusCode import sttp.tapir.server.tests.CreateServerTest import sttp.tapir.ztapir._ import zhttp.http._ import zio.{Task, ZIO} -import org.scalatest.matchers.should.Matchers._ class ZioHttpCompositionTest( createServerTest: CreateServerTest[Task, Any, ZioHttpServerOptions[Any], Http[Any, Throwable, zhttp.http.Request, zhttp.http.Response]] diff --git a/server/zio-http-server/src/test/scala/sttp/tapir/server/ziohttp/ZioHttpServerTest.scala b/server/zio-http-server/src/test/scala/sttp/tapir/server/ziohttp/ZioHttpServerTest.scala index c6c2a678ee..aaa20b5882 100644 --- a/server/zio-http-server/src/test/scala/sttp/tapir/server/ziohttp/ZioHttpServerTest.scala +++ b/server/zio-http-server/src/test/scala/sttp/tapir/server/ziohttp/ZioHttpServerTest.scala @@ -1,15 +1,20 @@ package sttp.tapir.server.ziohttp import cats.effect.{IO, Resource} +import io.netty.util.CharsetUtil +import org.scalatest.Assertion +import org.scalatest.matchers.should.Matchers._ import sttp.capabilities.zio.ZioStreams import sttp.monad.MonadError +import sttp.tapir._ import sttp.tapir.server.tests._ import sttp.tapir.tests.{Test, TestSuite} -import sttp.tapir.ztapir.RIOMonadError +import sttp.tapir.ztapir.{RIOMonadError, RichZEndpoint} +import zhttp.http.{Path, Request, URL} import zhttp.service.server.ServerChannelFactory import zhttp.service.{EventLoopGroup, ServerChannelFactory} import zio.interop.catz._ -import zio.{Runtime, Task, ZEnvironment} +import zio.{Runtime, Task, UIO, ZEnvironment, ZIO} class ZioHttpServerTest extends TestSuite { @@ -24,6 +29,19 @@ class ZioHttpServerTest extends TestSuite { val interpreter = new ZioHttpTestServerInterpreter(eventLoopGroup, channelFactory) val createServerTest = new DefaultCreateServerTest(backend, interpreter) + def additionalTests(): List[Test] = List( + // https://github.com/softwaremill/tapir/issues/1914 + Test("zio http route can be used as a function") { + val ep = endpoint.get.in("p1").out(stringBody).zServerLogic[Any](_ => ZIO.succeed("response")) + val route = ZioHttpInterpreter().toHttp(ep) + val test: UIO[Assertion] = route(Request(url = URL.apply(Path.empty / "p1"))) + .flatMap(response => response.data.toByteBuf.map(_.toString(CharsetUtil.UTF_8))) + .map(_ shouldBe "response") + .catchAll(_ => ZIO.succeed(fail("Unable to extract body from Http response"))) + zio.Runtime.default.unsafeRunToFuture(test) + } + ) + implicit val m: MonadError[Task] = new RIOMonadError[Any] new ServerBasicTests( @@ -39,7 +57,8 @@ class ZioHttpServerTest extends TestSuite { new AllServerTests(createServerTest, interpreter, backend, basic = false, staticContent = false, multipart = false, file = false) .tests() ++ new ServerStreamingTests(createServerTest, ZioStreams).tests() ++ - new ZioHttpCompositionTest(createServerTest).tests() + new ZioHttpCompositionTest(createServerTest).tests() ++ + additionalTests() } } } diff --git a/server/zio1-http-server/src/main/scala/sttp/tapir/server/ziohttp/ZioHttpInterpreter.scala b/server/zio1-http-server/src/main/scala/sttp/tapir/server/ziohttp/ZioHttpInterpreter.scala index 8200c1e3cc..5d7574f164 100644 --- a/server/zio1-http-server/src/main/scala/sttp/tapir/server/ziohttp/ZioHttpInterpreter.scala +++ b/server/zio1-http-server/src/main/scala/sttp/tapir/server/ziohttp/ZioHttpInterpreter.scala @@ -29,7 +29,7 @@ trait ZioHttpInterpreter[R] { zioHttpServerOptions.deleteFile ) - Http.route[Request] { case req => + Http.collectHttp[Request] { case req => Http .fromZIO( interpreter diff --git a/server/zio1-http-server/src/test/scala/sttp/tapir/server/ziohttp/ZioHttpServerTest.scala b/server/zio1-http-server/src/test/scala/sttp/tapir/server/ziohttp/ZioHttpServerTest.scala index 246180f576..65e0236634 100644 --- a/server/zio1-http-server/src/test/scala/sttp/tapir/server/ziohttp/ZioHttpServerTest.scala +++ b/server/zio1-http-server/src/test/scala/sttp/tapir/server/ziohttp/ZioHttpServerTest.scala @@ -1,15 +1,20 @@ package sttp.tapir.server.ziohttp import cats.effect.{IO, Resource} +import io.netty.util.CharsetUtil +import org.scalactic.source.Position.here +import org.scalatest.compatible.Assertion +import org.scalatest.matchers.should.Matchers._ import sttp.capabilities.zio.ZioStreams import sttp.monad.MonadError import sttp.tapir.server.tests._ import sttp.tapir.tests.{Test, TestSuite} -import sttp.tapir.ztapir.RIOMonadError +import sttp.tapir.ztapir._ +import zhttp.http._ import zhttp.service.server.ServerChannelFactory import zhttp.service.{EventLoopGroup, ServerChannelFactory} import zio.interop.catz._ -import zio.{Runtime, Task} +import zio.{Runtime, Task, UIO, ZIO} class ZioHttpServerTest extends TestSuite { @@ -21,6 +26,19 @@ class ZioHttpServerTest extends TestSuite { val interpreter = new ZioHttpTestServerInterpreter(nettyDeps) val createServerTest = new DefaultCreateServerTest(backend, interpreter) + def additionalTests(): List[Test] = List( + // https://github.com/softwaremill/tapir/issues/1914 + Test("zio http route can be used as a function") { + val ep = endpoint.get.in("p1").out(stringBody).zServerLogic[Any](_ => ZIO.succeed("response")) + val route = ZioHttpInterpreter().toHttp(ep) + val test: UIO[Assertion] = route(Request(url = URL.apply(Path.empty / "p1"))) + .flatMap(response => response.data.toByteBuf.map(_.toString(CharsetUtil.UTF_8))) + .map(_ shouldBe "response") + .catchAll(_ => ZIO.succeed(fail("Unable to extract body from Http response"))) + zio.Runtime.default.unsafeRunToFuture(test) + } + ) + implicit val m: MonadError[Task] = new RIOMonadError[Any] new ServerBasicTests( @@ -37,7 +55,9 @@ class ZioHttpServerTest extends TestSuite { new AllServerTests(createServerTest, interpreter, backend, basic = false, staticContent = false, multipart = false, file = true) .tests() ++ new ServerStreamingTests(createServerTest, ZioStreams).tests() ++ - new ZioHttpCompositionTest(createServerTest).tests() + new ZioHttpCompositionTest(createServerTest).tests() // ++ + // TODO: only works with zio2 + // additionalTests() } } }