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! :)
-
||||
| :---: | :---: | :---: |
| | | |
@@ -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!
@@ -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!
@@ -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()
}
}
}