diff --git a/.scalafix.conf b/.scalafix.conf index 25e991dbe3..40c578c6f6 100644 --- a/.scalafix.conf +++ b/.scalafix.conf @@ -1,2 +1,3 @@ OrganizeImports.groupedImports = AggressiveMerge -OrganizeImports.targetDialect = Scala3 \ No newline at end of file +OrganizeImports.targetDialect = Scala3 +OrganizeImports.removeUnused = false \ No newline at end of file diff --git a/README.md b/README.md index ef3cf3284e..2d1a7f68b4 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,8 @@ Please email us at [tapir@softwaremill.com](mailto:tapir@softwaremill.com) from | Broad | Kensu | Colisweb | | iceo | dpg | hunters | | moia | pits | hootsuite | +| ematiq | fugo | budgetbakers | +| | flo | | ## Teaser @@ -137,7 +139,7 @@ val booksListingRequest: Request[DecodeResult[Either[String, List[Book]]], Any] Add the following dependency: ```sbt -"com.softwaremill.sttp.tapir" %% "tapir-core" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-core" % "1.11.10" ``` Then, import: @@ -204,6 +206,8 @@ Fetch the tags from the upstream: git fetch --tags upstream ``` +When you have a PR ready, take a look at our ["How to prepare a good PR" guide](https://softwaremill.community/t/how-to-prepare-a-good-pr-to-a-library/448). Thanks! :) + ## Scoping which projects are included by `sbt` * when `STTP_NATIVE` is set, Scala native projects are included in the build (when running `sbt`) diff --git a/build.sbt b/build.sbt index d7a12b8bf3..530cf8c0e3 100644 --- a/build.sbt +++ b/build.sbt @@ -85,6 +85,8 @@ val commonSettings = commonSmlBuildSettings ++ ossPublishSettings ++ Seq( case _ => Seq("-Xmax-inlines", "64") } }, + Test / scalacOptions += "-Wconf:msg=unused value of type org.scalatest.Assertion:s", + Test / scalacOptions += "-Wconf:msg=unused value of type org.scalatest.compatible.Assertion:s", evictionErrorLevel := Level.Info ) @@ -422,7 +424,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.3.7") + Seq("com.softwaremill.magnolia1_3" %%% "magnolia" % "1.3.8") case _ => Seq( "com.softwaremill.magnolia1_2" %%% "magnolia" % "1.1.10", @@ -525,10 +527,10 @@ lazy val perfTests: ProjectMatrix = (projectMatrix in file("perf-tests")) "jackson-databind" ), "io.gatling" % "gatling-test-framework" % "3.11.5" % "test" exclude ("com.fasterxml.jackson.core", "jackson-databind"), - "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.17.2", + "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.18.2", "nl.grons" %% "metrics4-scala" % Versions.metrics4Scala % Test, "com.lihaoyi" %% "scalatags" % Versions.scalaTags % Test, - "io.github.classgraph" % "classgraph" % "4.8.176", + "io.github.classgraph" % "classgraph" % "4.8.179", "org.http4s" %% "http4s-core" % Versions.http4s, "org.http4s" %% "http4s-dsl" % Versions.http4s, "org.http4s" %% "http4s-blaze-server" % Versions.http4sBlazeServer, @@ -547,6 +549,7 @@ lazy val perfTests: ProjectMatrix = (projectMatrix in file("perf-tests")) .jvmPlatform(scalaVersions = List(scala2_13), settings = commonJvmSettings) .dependsOn( core, + circeJson, pekkoHttpServer, http4sServer, nettyServer, @@ -996,7 +999,7 @@ lazy val pekkoGrpcExamples: ProjectMatrix = (projectMatrix in file("grpc/pekko-e .settings( name := "tapir-pekko-grpc-examples", libraryDependencies ++= Seq( - "org.apache.pekko" %% "pekko-discovery" % "1.1.1", + "org.apache.pekko" %% "pekko-discovery" % "1.1.2", slf4j ), fork := true @@ -1016,8 +1019,8 @@ lazy val prometheusMetrics: ProjectMatrix = (projectMatrix in file("metrics/prom .settings( name := "tapir-prometheus-metrics", libraryDependencies ++= Seq( - "io.prometheus" % "prometheus-metrics-core" % "1.3.1", - "io.prometheus" % "prometheus-metrics-exposition-formats" % "1.3.1", + "io.prometheus" % "prometheus-metrics-core" % "1.3.5", + "io.prometheus" % "prometheus-metrics-exposition-formats" % "1.3.5", scalaTest.value % Test ) ) @@ -1244,7 +1247,7 @@ lazy val pekkoGrpcServer: ProjectMatrix = (projectMatrix in file("server/pekko-g .settings( name := "tapir-pekko-grpc-server", libraryDependencies ++= Seq( - "org.apache.pekko" %% "pekko-grpc-runtime" % "1.0.2" + "org.apache.pekko" %% "pekko-grpc-runtime" % "1.1.1" ) ) .jvmPlatform(scalaVersions = scala2And3Versions, settings = commonJvmSettings) @@ -1986,8 +1989,8 @@ lazy val openapiCodegenCore: ProjectMatrix = (projectMatrix in file("openapi-cod "com.47deg" %% "scalacheck-toolbox-datetime" % "0.7.0" % Test, scalaOrganization.value % "scala-reflect" % scalaVersion.value, scalaOrganization.value % "scala-compiler" % scalaVersion.value % Test, - "com.beachape" %% "enumeratum" % "1.7.4" % Test, - "com.beachape" %% "enumeratum-circe" % "1.7.4" % Test, + "com.beachape" %% "enumeratum" % "1.7.5" % Test, + "com.beachape" %% "enumeratum-circe" % "1.7.5" % Test, "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % "2.28.2" % Test, "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % "2.28.2" % Provided ) diff --git a/core/src/main/scala/sttp/tapir/attribute.scala b/core/src/main/scala/sttp/tapir/attribute.scala index 2295bd0a04..b5bd627003 100644 --- a/core/src/main/scala/sttp/tapir/attribute.scala +++ b/core/src/main/scala/sttp/tapir/attribute.scala @@ -2,6 +2,8 @@ package sttp.tapir import sttp.tapir.macros.AttributeKeyMacros +// TODO: use AttributeKey & AttributeMap from sttp-shared in Tapir2 + /** @param typeName * The fully qualified name of `T`. * @tparam T diff --git a/core/src/main/scala/sttp/tapir/macros/CreateDerivedEnumerationCodec.scala b/core/src/main/scala/sttp/tapir/macros/CreateDerivedEnumerationCodec.scala index dfb618307e..fa9a726a6c 100644 --- a/core/src/main/scala/sttp/tapir/macros/CreateDerivedEnumerationCodec.scala +++ b/core/src/main/scala/sttp/tapir/macros/CreateDerivedEnumerationCodec.scala @@ -30,7 +30,7 @@ class CreateDerivedEnumerationCodec[L, T](validator: Validator.Enumeration[T], s .mapDecode(s => decode(s) match { case Some(value) => DecodeResult.Value(value) - case None => DecodeResult.InvalidValue(List(ValidationError(validator, s))) + case None => DecodeResult.InvalidValue(List(ValidationError(v, s))) } )(encode) .schema(schemaAnnotations.enrich(s)) diff --git a/doc/adopters.md b/doc/adopters.md index 6b86241a9a..c35a9403f2 100644 --- a/doc/adopters.md +++ b/doc/adopters.md @@ -41,3 +41,13 @@ Thank you! pits hootsuite +
+ematiq +fugo +budgetbakers +
+
+  +flo +  +
\ No newline at end of file diff --git a/doc/adopters/budgetbakers.svg b/doc/adopters/budgetbakers.svg new file mode 100644 index 0000000000..809689faea --- /dev/null +++ b/doc/adopters/budgetbakers.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/adopters/ematiq.png b/doc/adopters/ematiq.png new file mode 100644 index 0000000000..b15347a988 Binary files /dev/null and b/doc/adopters/ematiq.png differ diff --git a/doc/adopters/flo.svg b/doc/adopters/flo.svg new file mode 100644 index 0000000000..c9a763252c --- /dev/null +++ b/doc/adopters/flo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/doc/adopters/fugo.png b/doc/adopters/fugo.png new file mode 100644 index 0000000000..d66b2cd43b Binary files /dev/null and b/doc/adopters/fugo.png differ diff --git a/doc/client/sttp.md b/doc/client/sttp.md index 7ebdc94967..8d99c78798 100644 --- a/doc/client/sttp.md +++ b/doc/client/sttp.md @@ -55,7 +55,7 @@ A => I => Request[DecodeResult[Either[E, O]], R] A => I => F[DecodeResult[Either[E, O]]] ``` -See the [runnable example](https://github.com/softwaremill/tapir/blob/master/examples/src/main/scala/sttp/tapir/examples/BooksExample.scala) +See the [runnable example](https://github.com/softwaremill/tapir/blob/master/examples/src/main/scala/sttp/tapir/examples/booksExample.scala) for example usage. ## Web sockets diff --git a/doc/how-tos/delimited-path-parameters.md b/doc/how-tos/delimited-path-parameters.md new file mode 100644 index 0000000000..9e975e2f8e --- /dev/null +++ b/doc/how-tos/delimited-path-parameters.md @@ -0,0 +1,103 @@ +# Handling Delimited Path Parameters + +Tapir allows you to handle complex path parameters, such as lists of custom types separated by delimiters (e.g., commas). +This can be achieved using `Codec.delimited`, which facilitates the serialization and deserialization of delimited lists +within path segments. + +## Use Case + +Suppose you want to define an endpoint that accepts a list of names as a comma-separated path parameter. Each name should +adhere to a specific pattern (e.g., only uppercase letters). + +## Implementation Steps: + +### 1. Define the Custom Type and Validator +Start by defining your custom type and the associated validator to enforce the desired pattern. + +```scala mdoc:compile-only +import sttp.tapir._ +import sttp.tapir.generic.auto._ +import sttp.tapir.Codec +import sttp.tapir.Validator +import sttp.tapir.CodecFormat.TextPlain +import sttp.tapir.model.Delimited + +case class Name(value: String) + +// Validator to ensure names consist of uppercase letters only +val nameValidator: Validator[String] = Validator.pattern("^[A-Z]+$") +``` + +### 2. Create Codecs for the Custom Type and Delimited List +Utilize `Codec.parsedString` for individual `Name` instances and `Codec.delimited` for handling the list. + +```scala +// Codec for single Name +given Codec[String, Name, TextPlain] = Codec.parsedString(Name.apply) + .validate(nameValidator.contramap(_.value)) + +// Codec for a list of Names, delimited by commas +given Codec[String, Delimited[",", Name], TextPlain] = Codec.delimited +``` + +### 3. Define the Endpoint with Delimited Path Parameter +Incorporate the delimited codec into your endpoint definition to handle the list of names in the path. + +```scala mdoc:compile-only +import sttp.tapir._ +import sttp.tapir.generic.auto._ +import sttp.tapir.Codec +import sttp.tapir.Validator +import sttp.tapir.CodecFormat.TextPlain +import sttp.tapir.model.Delimited + +case class Name(value: String) + +// Validator to ensure names consist of uppercase letters only +val nameValidator: Validator[String] = Validator.pattern("^[A-Z]+$") + +// Codec for single Name +given Codec[String, Name, TextPlain] = Codec.parsedString(Name.apply) + .validate(nameValidator.contramap(_.value)) + +// Codec for a list of Names, delimited by commas +given Codec[String, Delimited[",", Name], TextPlain] = Codec.delimited + +val getUserEndpoint = + endpoint.get + .in("user" / path[Delimited[",", Name]]("id")) + .out(stringBody) +``` + +### 4. Generated OpenAPI Schema +When you generate the OpenAPI documentation for this endpoint, the schema for the `id` path parameter will +correctly reflect it as an array with the specified pattern for each item. + +```yaml +paths: + /user/{id}: + get: + operationId: getUserId + parameters: + - name: id + in: path + required: true + schema: + type: array + items: + type: string + pattern: ^[A-Z]+$ +``` + +## Explanation +- `Codec.parsedString`: Transforms a `String` into a custom type (`Name`) and vice versa. It also applies validation to + ensure each `Name` adheres to the specified pattern. +- `Codec.delimited`: Handles the serialization and deserialization of a delimited list (e.g., comma-separated) of the + custom type. By specifying `Delimited[",", Name]`, Tapir knows how to split and join the list based on the delimiter. +- Endpoint Definition: The `path[List[Name]]("id")` indicates that the id path parameter should be treated as a list of + `Name` objects, utilizing the previously defined codecs. + +## Validation +Validators play a crucial role in ensuring that each element within the delimited list meets the required criteria. In +this example, `nameValidator` ensures that each `Name` consists solely of uppercase letters. Tapir applies this validation +to each element in the list, providing robust input validation. \ No newline at end of file diff --git a/doc/index.md b/doc/index.md index 9fe3f0bbe4..b57ef23384 100644 --- a/doc/index.md +++ b/doc/index.md @@ -156,6 +156,7 @@ sttp is a family of Scala HTTP-related projects, and currently includes: examples external + how-tos/delimited-path-parameters .. toctree:: :maxdepth: 2 diff --git a/doc/other/stability.md b/doc/other/stability.md index 69a847375c..5e3f9f0038 100644 --- a/doc/other/stability.md +++ b/doc/other/stability.md @@ -6,6 +6,12 @@ The modules are categorised using the following levels: * **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 +The major version is increased when there are binary-incompatible changes in **stable** modules. + +The minor version is increased when there are significant new features in **stable** modules (keeping compatibility), or binary-incompatible changes in **stabilising** modules. + +The patch version is increased when there are binary-compatible changes in **stable** / **stabilising** modules, any changes in **exeperimental** modules, or when a new module is added (e.g. a new integration). + ## Main modules | Module | Level | diff --git a/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala index 3f477b7150..892a7d4d22 100644 --- a/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala @@ -1,8 +1,8 @@ // {cat=Hello, World!; effects=cats-effect; server=http4s}: Exposing an endpoint using the http4s server -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.5 -//> using dep com.softwaremill.sttp.client3::core:3.9.7 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.10 +//> using dep com.softwaremill.sttp.client3::core:3.9.8 //> using dep org.http4s::http4s-blaze-server:0.23.16 package sttp.tapir.examples diff --git a/examples/src/main/scala/sttp/tapir/examples/HelloWorldNettyCatsServer.scala b/examples/src/main/scala/sttp/tapir/examples/HelloWorldNettyCatsServer.scala index ed92cb1115..99422815f6 100644 --- a/examples/src/main/scala/sttp/tapir/examples/HelloWorldNettyCatsServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/HelloWorldNettyCatsServer.scala @@ -1,8 +1,8 @@ // {cat=Hello, World!; effects=cats-effect; server=Netty}: Exposing an endpoint using the Netty server (cats-effect variant) -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-cats:1.11.5 -//> using dep com.softwaremill.sttp.client3::core:3.9.7 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-cats:1.11.10 +//> using dep com.softwaremill.sttp.client3::core:3.9.8 //> using dep ch.qos.logback:logback-classic:1.5.8 package sttp.tapir.examples diff --git a/examples/src/main/scala/sttp/tapir/examples/HelloWorldZioHttpServer.scala b/examples/src/main/scala/sttp/tapir/examples/HelloWorldZioHttpServer.scala index a6b71db7b6..3bd90af868 100644 --- a/examples/src/main/scala/sttp/tapir/examples/HelloWorldZioHttpServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/HelloWorldZioHttpServer.scala @@ -1,8 +1,8 @@ // {cat=Hello, World!; effects=ZIO; server=ZIO HTTP; json=ZIO JSON}: Exposing an endpoint using the ZIO HTTP server -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-json-zio:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-zio-http-server:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-json-zio:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-zio-http-server:1.11.10 package sttp.tapir.examples diff --git a/examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala index deeacea67b..410c91b98b 100644 --- a/examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala @@ -1,11 +1,11 @@ // {cat=Hello, World!; effects=ZIO; server=http4s; json=circe; docs=Swagger UI}: Exposing an endpoint, defined with ZIO and depending on services in the environment, using the http4s server //> using option -Ykind-projector -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server-zio:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-zio:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server-zio:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-zio:1.11.10 //> using dep org.http4s::http4s-blaze-server:0.23.16 //> using dep dev.zio::zio-interop-cats:23.1.0.3 diff --git a/examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala index fc7d49f647..5c26d28f34 100644 --- a/examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala @@ -1,10 +1,10 @@ // {cat=Hello, World!; effects=ZIO; server=http4s; json=circe; docs=Swagger UI}: Exposing an endpoint using the http4s server -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server-zio:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-zio:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server-zio:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-zio:1.11.10 //> using dep org.http4s::http4s-blaze-server:0.23.16 //> using dep dev.zio::zio-interop-cats:23.1.0.3 diff --git a/examples/src/main/scala/sttp/tapir/examples/ZioExampleZioHttpServer.scala b/examples/src/main/scala/sttp/tapir/examples/ZioExampleZioHttpServer.scala index 13ca989d99..c634e54f87 100644 --- a/examples/src/main/scala/sttp/tapir/examples/ZioExampleZioHttpServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/ZioExampleZioHttpServer.scala @@ -1,11 +1,11 @@ // {cat=Hello, World!; effects=ZIO; server=zio-http; json=circe; docs=Swagger UI}: Exposing an endpoint using the ZIO HTTP server //> using option -Ykind-projector -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-zio-http-server:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-zio:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-zio-http-server:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-zio:1.11.10 package sttp.tapir.examples diff --git a/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala b/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala index 10ac5e30e5..0031aaadf4 100644 --- a/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala +++ b/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala @@ -1,11 +1,11 @@ // {cat=Hello, World!; effects=ZIO; server=http4s}: Extending a base endpoint (which has the security logic provided), with server logic //> using option -Ykind-projector -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server-zio:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-zio:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server-zio:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-zio:1.11.10 //> using dep org.http4s::http4s-blaze-server:0.23.16 -//> using dep com.softwaremill.sttp.client3::async-http-client-backend-zio:3.9.8 +//> using dep com.softwaremill.sttp.client3::async-http-client-backend-zio:3.10.1 package sttp.tapir.examples diff --git a/examples/src/main/scala/sttp/tapir/examples/booksExample.scala b/examples/src/main/scala/sttp/tapir/examples/booksExample.scala index f35649bd99..fb6ef509d1 100644 --- a/examples/src/main/scala/sttp/tapir/examples/booksExample.scala +++ b/examples/src/main/scala/sttp/tapir/examples/booksExample.scala @@ -1,10 +1,10 @@ // {cat=Hello, World!; effects=Future; server=Pekko HTTP; client=sttp3; JSON=circe; docs=Swagger UI}: A demo of Tapir's capabilities -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-sttp-client:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-sttp-client:1.11.10 //> using dep org.apache.pekko::pekko-http:1.0.1 //> using dep org.apache.pekko::pekko-stream:1.0.3 //> using dep ch.qos.logback:logback-classic:1.5.6 diff --git a/examples/src/main/scala/sttp/tapir/examples/booksPicklerExample.scala b/examples/src/main/scala/sttp/tapir/examples/booksPicklerExample.scala index 4db4c08e6c..634211c84b 100644 --- a/examples/src/main/scala/sttp/tapir/examples/booksPicklerExample.scala +++ b/examples/src/main/scala/sttp/tapir/examples/booksPicklerExample.scala @@ -1,10 +1,10 @@ // {cat=Hello, World!; effects=Future; server=Netty; client=sttp3; JSON=Pickler; docs=Swagger UI}: A demo of Tapir's capabilities -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-json-pickler:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-sttp-client:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-json-pickler:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-sttp-client:1.11.10 //> using dep org.apache.pekko::pekko-http:1.0.1 //> using dep org.apache.pekko::pekko-stream:1.0.3 //> using dep ch.qos.logback:logback-classic:1.5.6 diff --git a/examples/src/main/scala/sttp/tapir/examples/client/Http4sClientExample.scala b/examples/src/main/scala/sttp/tapir/examples/client/Http4sClientExample.scala index c3aa9a97c9..d82f7f91a1 100644 --- a/examples/src/main/scala/sttp/tapir/examples/client/Http4sClientExample.scala +++ b/examples/src/main/scala/sttp/tapir/examples/client/Http4sClientExample.scala @@ -1,8 +1,8 @@ // {cat=Client interpreter; effects=cats-effect; JSON=circe}: Interpreting an endpoint as an http4s client -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-http4s-client:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-http4s-client:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.10 //> using dep org.http4s::http4s-circe:0.23.27 //> using dep org.http4s::http4s-blaze-server:0.23.16 //> using dep org.http4s::http4s-dsl:0.23.27 diff --git a/examples/src/main/scala/sttp/tapir/examples/custom_types/EndpointWithCustomTypes.scala b/examples/src/main/scala/sttp/tapir/examples/custom_types/EndpointWithCustomTypes.scala index 6a8e8084e2..874d65e12e 100644 --- a/examples/src/main/scala/sttp/tapir/examples/custom_types/EndpointWithCustomTypes.scala +++ b/examples/src/main/scala/sttp/tapir/examples/custom_types/EndpointWithCustomTypes.scala @@ -1,7 +1,7 @@ // {cat=Custom types; json=circe}: Supporting custom types, when used in query or path parameters, as well as part of JSON bodies -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.10 package sttp.tapir.examples.custom_types diff --git a/examples/src/main/scala/sttp/tapir/examples/custom_types/booksExampleSemiauto.scala b/examples/src/main/scala/sttp/tapir/examples/custom_types/booksExampleSemiauto.scala index 2944dad24f..943bf6bb70 100644 --- a/examples/src/main/scala/sttp/tapir/examples/custom_types/booksExampleSemiauto.scala +++ b/examples/src/main/scala/sttp/tapir/examples/custom_types/booksExampleSemiauto.scala @@ -1,10 +1,10 @@ // {cat=Custom types; effects=Future; server=Pekko HTTP; client=sttp3; JSON=circe; docs=Swagger UI}: A demo of Tapir's capabilities using semi-auto derivation -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-sttp-client:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-sttp-client:1.11.10 //> using dep org.apache.pekko::pekko-http:1.0.1 //> using dep org.apache.pekko::pekko-stream:1.0.3 diff --git a/examples/src/main/scala/sttp/tapir/examples/custom_types/commaSeparatedQueryParameter.scala b/examples/src/main/scala/sttp/tapir/examples/custom_types/commaSeparatedQueryParameter.scala index 586b419473..86d0b4df9d 100644 --- a/examples/src/main/scala/sttp/tapir/examples/custom_types/commaSeparatedQueryParameter.scala +++ b/examples/src/main/scala/sttp/tapir/examples/custom_types/commaSeparatedQueryParameter.scala @@ -1,8 +1,8 @@ // {cat=Custom types; effects=Direct; server=Netty; docs=Swagger UI}: Handling comma-separated query parameters -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.10 package sttp.tapir.examples.custom_types diff --git a/examples/src/main/scala/sttp/tapir/examples/custom_types/enumQueryParameter.scala b/examples/src/main/scala/sttp/tapir/examples/custom_types/enumQueryParameter.scala new file mode 100644 index 0000000000..1842d937d8 --- /dev/null +++ b/examples/src/main/scala/sttp/tapir/examples/custom_types/enumQueryParameter.scala @@ -0,0 +1,27 @@ +// {cat=Custom types; effects=Direct; server=Netty}: A query parameter which maps to a Scala 3 enum (enumeration) + +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.10 +//> using dep ch.qos.logback:logback-classic:1.5.8 + +package sttp.tapir.examples.custom_types + +import sttp.tapir.* +import sttp.tapir.server.netty.sync.NettySyncServer + +enum PieType(val index: Int, val name: String): + case Apple extends PieType(1, "apple") + case Orange extends PieType(2, "orange") + case BananaCustard extends PieType(3, "banana-custard") + +@main def enumQueryParameter(): Unit = + given Codec[String, PieType, CodecFormat.TextPlain] = + Codec.derivedEnumeration[String, PieType](decode = s => PieType.values.find(_.name == s), encode = _.name) + + val bake = endpoint.get + .in("bake") + .in(query[PieType]("pie")) + .out(stringBody) + .handleSuccess(pie => s"Baking: $pie!") + + NettySyncServer().addEndpoint(bake).startAndWait() diff --git a/examples/src/main/scala/sttp/tapir/examples/custom_types/sealedTraitWithDiscriminator.scala b/examples/src/main/scala/sttp/tapir/examples/custom_types/sealedTraitWithDiscriminator.scala index fccefa5f80..4269760b7d 100644 --- a/examples/src/main/scala/sttp/tapir/examples/custom_types/sealedTraitWithDiscriminator.scala +++ b/examples/src/main/scala/sttp/tapir/examples/custom_types/sealedTraitWithDiscriminator.scala @@ -1,9 +1,9 @@ // {cat=Custom types; effects=Direct; server=Netty; JSON=circe; docs=Swagger UI}: Mapping a sealed trait hierarchy to JSON using a discriminator -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.10 package sttp.tapir.examples.custom_types diff --git a/examples/src/main/scala/sttp/tapir/examples/errors/ErrorUnionTypesHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/errors/ErrorUnionTypesHttp4sServer.scala index 0c65409406..026dda492b 100644 --- a/examples/src/main/scala/sttp/tapir/examples/errors/ErrorUnionTypesHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/errors/ErrorUnionTypesHttp4sServer.scala @@ -1,10 +1,10 @@ // {cat=Error handling; effects=cats-effect; server=http4s; JSON=circe}: Extending a base secured endpoint with error variants, using union types -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.10 //> using dep org.http4s::http4s-blaze-server:0.23.16 -//> using dep com.softwaremill.sttp.client3::core:3.9.7 +//> using dep com.softwaremill.sttp.client3::core:3.9.8 package sttp.tapir.examples.errors diff --git a/examples/src/main/scala/sttp/tapir/examples/errors/IronRefinementErrorsNettyServer.scala b/examples/src/main/scala/sttp/tapir/examples/errors/IronRefinementErrorsNettyServer.scala index a788ba910c..6650b966af 100644 --- a/examples/src/main/scala/sttp/tapir/examples/errors/IronRefinementErrorsNettyServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/errors/IronRefinementErrorsNettyServer.scala @@ -1,10 +1,10 @@ // {cat=Error handling; effects=cats-effect; server=Netty; JSON=circe}: Error reporting provided by Iron type refinements -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-cats:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-iron:1.11.5 -//> using dep com.softwaremill.sttp.client3::core:3.9.7 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-cats:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-iron:1.11.10 +//> using dep com.softwaremill.sttp.client3::core:3.9.8 package sttp.tapir.examples.errors diff --git a/examples/src/main/scala/sttp/tapir/examples/errors/customErrorsOnDecodeFailurePekkoServer.scala b/examples/src/main/scala/sttp/tapir/examples/errors/customErrorsOnDecodeFailurePekkoServer.scala index c16a97e403..cd41e5f437 100644 --- a/examples/src/main/scala/sttp/tapir/examples/errors/customErrorsOnDecodeFailurePekkoServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/errors/customErrorsOnDecodeFailurePekkoServer.scala @@ -1,10 +1,10 @@ // {cat=Error handling; effects=Future; server=Pekko HTTP}: Customising errors that are reported on decode failures (e.g. invalid or missing query parameter) -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.11.10 //> using dep org.apache.pekko::pekko-http:1.0.1 //> using dep org.apache.pekko::pekko-stream:1.0.3 -//> using dep com.softwaremill.sttp.client3::core:3.9.7 +//> using dep com.softwaremill.sttp.client3::core:3.9.8 package sttp.tapir.examples.errors diff --git a/examples/src/main/scala/sttp/tapir/examples/errors/errorOutputsPekkoServer.scala b/examples/src/main/scala/sttp/tapir/examples/errors/errorOutputsPekkoServer.scala index 86643f45f8..0ef5e71c7a 100644 --- a/examples/src/main/scala/sttp/tapir/examples/errors/errorOutputsPekkoServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/errors/errorOutputsPekkoServer.scala @@ -1,11 +1,11 @@ // {cat=Error handling; effects=Future; server=Pekko HTTP; json=circe}: Error and successful outputs -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.10 //> using dep org.apache.pekko::pekko-http:1.0.1 //> using dep org.apache.pekko::pekko-stream:1.0.3 -//> using dep com.softwaremill.sttp.client3::core:3.9.7 +//> using dep com.softwaremill.sttp.client3::core:3.9.8 package sttp.tapir.examples.errors diff --git a/examples/src/main/scala/sttp/tapir/examples/helloWorldArmeriaServer.scala b/examples/src/main/scala/sttp/tapir/examples/helloWorldArmeriaServer.scala index fb3d1de893..52cc77b496 100644 --- a/examples/src/main/scala/sttp/tapir/examples/helloWorldArmeriaServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/helloWorldArmeriaServer.scala @@ -1,8 +1,8 @@ // {cat=Hello, World!; effects=Future; server=Armeria}: Exposing an endpoint using the Armeria server -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-armeria-server:1.11.5 -//> using dep com.softwaremill.sttp.client3::core:3.9.7 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-armeria-server:1.11.10 +//> using dep com.softwaremill.sttp.client3::core:3.9.8 package sttp.tapir.examples diff --git a/examples/src/main/scala/sttp/tapir/examples/helloWorldJdkHttpServer.scala b/examples/src/main/scala/sttp/tapir/examples/helloWorldJdkHttpServer.scala index 78654b193f..8a708e30b7 100644 --- a/examples/src/main/scala/sttp/tapir/examples/helloWorldJdkHttpServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/helloWorldJdkHttpServer.scala @@ -1,8 +1,8 @@ // {cat=Hello, World!; effects=Direct; server=JDK Http}: Exposing an endpoint using the built-in JDK HTTP server -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-jdkhttp-server:1.11.5 -//> using dep com.softwaremill.sttp.client3::core:3.9.7 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-jdkhttp-server:1.11.10 +//> using dep com.softwaremill.sttp.client3::core:3.9.8 package sttp.tapir.examples diff --git a/examples/src/main/scala/sttp/tapir/examples/helloWorldNettyFutureServer.scala b/examples/src/main/scala/sttp/tapir/examples/helloWorldNettyFutureServer.scala index 8ccca12bdc..7a7a47f222 100644 --- a/examples/src/main/scala/sttp/tapir/examples/helloWorldNettyFutureServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/helloWorldNettyFutureServer.scala @@ -1,8 +1,8 @@ // {cat=Hello, World!; effects=Future; server=Netty}: Exposing an endpoint using the Netty server (Future variant) -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server:1.11.5 -//> using dep com.softwaremill.sttp.client3::core:3.9.7 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server:1.11.10 +//> using dep com.softwaremill.sttp.client3::core:3.9.8 //> using dep ch.qos.logback:logback-classic:1.5.8 package sttp.tapir.examples diff --git a/examples/src/main/scala/sttp/tapir/examples/helloWorldNettySyncServer.scala b/examples/src/main/scala/sttp/tapir/examples/helloWorldNettySyncServer.scala index 24c23613e8..79d9e1d190 100644 --- a/examples/src/main/scala/sttp/tapir/examples/helloWorldNettySyncServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/helloWorldNettySyncServer.scala @@ -1,7 +1,7 @@ // {cat=Hello, World!; effects=Direct; server=Netty}: Exposing an endpoint using the Netty server (Direct-style variant) -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.10 //> using dep ch.qos.logback:logback-classic:1.5.8 package sttp.tapir.examples @@ -30,6 +30,6 @@ import sttp.tapir.server.netty.sync.NettySyncServer supervised { val serverBinding = useInScope(NettySyncServer().addEndpoint(helloWorld).start())(_.stop()) - println(s"Tapir is running on port ${serverBinding.port}") + println(s"You can now make requests to http://${serverBinding.hostName}:${serverBinding.port}/hello?name=...!") never } diff --git a/examples/src/main/scala/sttp/tapir/examples/helloWorldPekkoServer.scala b/examples/src/main/scala/sttp/tapir/examples/helloWorldPekkoServer.scala index 7be680c080..2e58a5ca8e 100644 --- a/examples/src/main/scala/sttp/tapir/examples/helloWorldPekkoServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/helloWorldPekkoServer.scala @@ -1,8 +1,8 @@ // {cat=Hello, World!; effects=Future; server=Pekko HTTP}: Exposing an endpoint using the Pekko HTTP server -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.11.5 -//> using dep com.softwaremill.sttp.client3::core:3.9.7 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.11.10 +//> using dep com.softwaremill.sttp.client3::core:3.9.8 package sttp.tapir.examples diff --git a/examples/src/main/scala/sttp/tapir/examples/json/circeAutoDerivationNettySyncServer.scala b/examples/src/main/scala/sttp/tapir/examples/json/circeAutoDerivationNettySyncServer.scala index fe2bfc4a70..20496ab25e 100644 --- a/examples/src/main/scala/sttp/tapir/examples/json/circeAutoDerivationNettySyncServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/json/circeAutoDerivationNettySyncServer.scala @@ -1,8 +1,8 @@ // {cat=JSON; effects=Direct; server=Netty; JSON=circe}: Return a JSON response with Circe and auto-dervied codecs -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.10 //> using dep ch.qos.logback:logback-classic:1.5.8 package sttp.tapir.examples.json diff --git a/examples/src/main/scala/sttp/tapir/examples/json/circeNullBody.scala b/examples/src/main/scala/sttp/tapir/examples/json/circeNullBody.scala index 6f70571ccf..2937f3431d 100644 --- a/examples/src/main/scala/sttp/tapir/examples/json/circeNullBody.scala +++ b/examples/src/main/scala/sttp/tapir/examples/json/circeNullBody.scala @@ -1,8 +1,8 @@ // {cat=JSON; effects=Direct; server=Netty; JSON=circe}: Return a JSON body which optionally serializes as `null` -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.10 //> using dep ch.qos.logback:logback-classic:1.5.8 package sttp.tapir.examples.json diff --git a/examples/src/main/scala/sttp/tapir/examples/json/jsoniterNettySyncServer.scala b/examples/src/main/scala/sttp/tapir/examples/json/jsoniterNettySyncServer.scala index bbfd169b2d..d9249a77bc 100644 --- a/examples/src/main/scala/sttp/tapir/examples/json/jsoniterNettySyncServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/json/jsoniterNettySyncServer.scala @@ -1,10 +1,10 @@ // {cat=JSON; effects=Direct; server=Netty; JSON=jsoniter}: Return a JSON response with Jsoniter -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-jsoniter-scala:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-jsoniter-scala:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.10 //> using dep ch.qos.logback:logback-classic:1.5.8 -//> using dep com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros:2.30.11 +//> using dep com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros:2.30.15 package sttp.tapir.examples.json diff --git a/examples/src/main/scala/sttp/tapir/examples/logging/ZioLoggingWithCorrelationIdNettyServer.scala b/examples/src/main/scala/sttp/tapir/examples/logging/ZioLoggingWithCorrelationIdNettyServer.scala index 5d5d60dd87..0128aced93 100644 --- a/examples/src/main/scala/sttp/tapir/examples/logging/ZioLoggingWithCorrelationIdNettyServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/logging/ZioLoggingWithCorrelationIdNettyServer.scala @@ -1,8 +1,8 @@ // {cat=Logging; effects=ZIO; server=Netty}: Logging using a correlation id -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-zio:1.11.5 -//> using dep com.softwaremill.sttp.client3::zio:3.9.7 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-zio:1.11.10 +//> using dep com.softwaremill.sttp.client3::zio:3.9.8 package sttp.tapir.examples.logging diff --git a/examples/src/main/scala/sttp/tapir/examples/multipart/multipartFormUploadPekkoServer.scala b/examples/src/main/scala/sttp/tapir/examples/multipart/multipartFormUploadPekkoServer.scala index 1fb8af8f5c..4d06c907b6 100644 --- a/examples/src/main/scala/sttp/tapir/examples/multipart/multipartFormUploadPekkoServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/multipart/multipartFormUploadPekkoServer.scala @@ -1,10 +1,10 @@ // {cat=Multipart; effects=Future; server=Pekko HTTP}: Uploading a multipart form, with text and file parts -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.11.10 //> using dep org.apache.pekko::pekko-http:1.0.1 //> using dep org.apache.pekko::pekko-stream:1.0.3 -//> using dep com.softwaremill.sttp.client3::core:3.9.7 +//> using dep com.softwaremill.sttp.client3::core:3.9.8 package sttp.tapir.examples.multipart diff --git a/examples/src/main/scala/sttp/tapir/examples/observability/ZioMetricsExample.scala b/examples/src/main/scala/sttp/tapir/examples/observability/ZioMetricsExample.scala index e5a632c6f7..9d6d6a4daf 100644 --- a/examples/src/main/scala/sttp/tapir/examples/observability/ZioMetricsExample.scala +++ b/examples/src/main/scala/sttp/tapir/examples/observability/ZioMetricsExample.scala @@ -1,8 +1,8 @@ // {cat=Observability; effects=ZIO; server=ZIO HTTP}: Reporting Prometheus metrics -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-zio-http-server:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-zio-metrics:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-zio-http-server:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-zio-metrics:1.11.10 package sttp.tapir.examples.observability diff --git a/examples/src/main/scala/sttp/tapir/examples/observability/datadogMetricsExample.scala b/examples/src/main/scala/sttp/tapir/examples/observability/datadogMetricsExample.scala index 3f38fe9eec..2f98a46d73 100644 --- a/examples/src/main/scala/sttp/tapir/examples/observability/datadogMetricsExample.scala +++ b/examples/src/main/scala/sttp/tapir/examples/observability/datadogMetricsExample.scala @@ -1,9 +1,9 @@ // {cat=Observability; effects=Future; server=Netty; json=circe}: Reporting DataDog metrics -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-datadog-metrics:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-datadog-metrics:1.11.10 //> using dep org.slf4j:slf4j-api:2.0.13 package sttp.tapir.examples.observability diff --git a/examples/src/main/scala/sttp/tapir/examples/observability/openTelemetryMetricsExample.scala b/examples/src/main/scala/sttp/tapir/examples/observability/openTelemetryMetricsExample.scala index 54f72f20f1..3676507687 100644 --- a/examples/src/main/scala/sttp/tapir/examples/observability/openTelemetryMetricsExample.scala +++ b/examples/src/main/scala/sttp/tapir/examples/observability/openTelemetryMetricsExample.scala @@ -1,10 +1,10 @@ // {cat=Observability; effects=Future; server=Netty; json=circe}: Reporting OpenTelemetry metrics -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-opentelemetry-metrics:1.11.5 -//> using dep io.opentelemetry:opentelemetry-exporter-otlp:1.42.1 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-opentelemetry-metrics:1.11.10 +//> using dep io.opentelemetry:opentelemetry-exporter-otlp:1.44.1 //> using dep org.slf4j:slf4j-api:2.0.13 package sttp.tapir.examples.observability diff --git a/examples/src/main/scala/sttp/tapir/examples/observability/prometheusMetricsExample.scala b/examples/src/main/scala/sttp/tapir/examples/observability/prometheusMetricsExample.scala index 5de85da3d8..4c159fcf2c 100644 --- a/examples/src/main/scala/sttp/tapir/examples/observability/prometheusMetricsExample.scala +++ b/examples/src/main/scala/sttp/tapir/examples/observability/prometheusMetricsExample.scala @@ -1,9 +1,9 @@ // {cat=Observability; effects=Future; server=Netty; json=circe}: Reporting Prometheus metrics -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-prometheus-metrics:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-prometheus-metrics:1.11.10 //> using dep org.slf4j:slf4j-api:2.0.13 package sttp.tapir.examples.observability diff --git a/examples/src/main/scala/sttp/tapir/examples/openapi/MultipleEndpointsDocumentationHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/openapi/MultipleEndpointsDocumentationHttp4sServer.scala index 12aeb06979..052ecda0bf 100644 --- a/examples/src/main/scala/sttp/tapir/examples/openapi/MultipleEndpointsDocumentationHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/openapi/MultipleEndpointsDocumentationHttp4sServer.scala @@ -1,9 +1,9 @@ // {cat=OpenAPI documentation; effects=cats-effect; server=http4s; docs=Swagger UI; json=circe}: Documenting multiple endpoints -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.10 //> using dep org.http4s::http4s-blaze-server:0.23.16 package sttp.tapir.examples.openapi diff --git a/examples/src/main/scala/sttp/tapir/examples/openapi/RedocContextPathHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/openapi/RedocContextPathHttp4sServer.scala index 3e634d8af0..19bdcc881e 100644 --- a/examples/src/main/scala/sttp/tapir/examples/openapi/RedocContextPathHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/openapi/RedocContextPathHttp4sServer.scala @@ -1,8 +1,8 @@ // {cat=OpenAPI documentation; effects=cats-effect; server=http4s; docs=ReDoc}: Exposing documentation using ReDoc -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-redoc-bundle:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-redoc-bundle:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.10 //> using dep org.http4s::http4s-blaze-server:0.23.16 package sttp.tapir.examples.openapi diff --git a/examples/src/main/scala/sttp/tapir/examples/openapi/RedocZioHttpServer.scala b/examples/src/main/scala/sttp/tapir/examples/openapi/RedocZioHttpServer.scala index 991879b291..60a77740b3 100644 --- a/examples/src/main/scala/sttp/tapir/examples/openapi/RedocZioHttpServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/openapi/RedocZioHttpServer.scala @@ -1,9 +1,9 @@ // {cat=OpenAPI documentation; effects=ZIO; server=ZIO HTTP; json=circe; docs=ReDoc}: Exposing documentation using ReDoc -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-redoc-bundle:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-zio-http-server:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-redoc-bundle:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-zio-http-server:1.11.10 package sttp.tapir.examples.openapi diff --git a/examples/src/main/scala/sttp/tapir/examples/openapi/multipleEndpointsDocumentationPekkoServer.scala b/examples/src/main/scala/sttp/tapir/examples/openapi/multipleEndpointsDocumentationPekkoServer.scala index a40e738984..56dce43e13 100644 --- a/examples/src/main/scala/sttp/tapir/examples/openapi/multipleEndpointsDocumentationPekkoServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/openapi/multipleEndpointsDocumentationPekkoServer.scala @@ -1,9 +1,9 @@ // {cat=OpenAPI documentation; effects=Future; server=Pekko HTTP; docs=Swagger UI; json=circe}: Documenting multiple endpoints -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.11.10 package sttp.tapir.examples.openapi diff --git a/examples/src/main/scala/sttp/tapir/examples/openapi/openapiExtensions.scala b/examples/src/main/scala/sttp/tapir/examples/openapi/openapiExtensions.scala index 3d078a491d..5cfd9043a1 100644 --- a/examples/src/main/scala/sttp/tapir/examples/openapi/openapiExtensions.scala +++ b/examples/src/main/scala/sttp/tapir/examples/openapi/openapiExtensions.scala @@ -1,8 +1,8 @@ // {cat=OpenAPI documentation; json=circe}: Adding OpenAPI documentation extensions -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-openapi-docs:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-openapi-docs:1.11.10 //> using dep com.softwaremill.sttp.apispec::openapi-circe-yaml:0.10.0 package sttp.tapir.examples.openapi diff --git a/examples/src/main/scala/sttp/tapir/examples/openapi/swaggerUIOAuth2PekkoServer.scala b/examples/src/main/scala/sttp/tapir/examples/openapi/swaggerUIOAuth2PekkoServer.scala index 9f3f8172e4..c4c8d645f4 100644 --- a/examples/src/main/scala/sttp/tapir/examples/openapi/swaggerUIOAuth2PekkoServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/openapi/swaggerUIOAuth2PekkoServer.scala @@ -1,8 +1,8 @@ // {cat=OpenAPI documentation; effects=Future; server=Pekko HTTP; docs=Swagger UI}: Securing Swagger UI using OAuth 2 -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.11.10 package sttp.tapir.examples.openapi diff --git a/examples/src/main/scala/sttp/tapir/examples/schema/customisingSchemas.scala b/examples/src/main/scala/sttp/tapir/examples/schema/customisingSchemas.scala index b8b5761748..72227d46da 100644 --- a/examples/src/main/scala/sttp/tapir/examples/schema/customisingSchemas.scala +++ b/examples/src/main/scala/sttp/tapir/examples/schema/customisingSchemas.scala @@ -1,9 +1,9 @@ // {cat=Schemas; effects=Future; server=Netty; json=circe; docs=Swagger UI}: Customising a derived schema, using annotations, and using implicits -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.10 package sttp.tapir.examples.schema diff --git a/examples/src/main/scala/sttp/tapir/examples/security/OAuth2GithubHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/security/OAuth2GithubHttp4sServer.scala index f1df2f220f..7d119d9928 100644 --- a/examples/src/main/scala/sttp/tapir/examples/security/OAuth2GithubHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/security/OAuth2GithubHttp4sServer.scala @@ -1,9 +1,9 @@ // {cat=Security; effects=cats-effect; server=http4s; json=circe}: Login using OAuth2, authorization code flow -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.5 -//> using dep com.softwaremill.sttp.client3::async-http-client-backend-cats:3.9.8 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.10 +//> using dep com.softwaremill.sttp.client3::async-http-client-backend-cats:3.10.1 //> using dep org.http4s::http4s-blaze-server:0.23.16 //> using dep com.github.jwt-scala::jwt-circe:10.0.1 diff --git a/examples/src/main/scala/sttp/tapir/examples/security/ServerSecurityLogicZio.scala b/examples/src/main/scala/sttp/tapir/examples/security/ServerSecurityLogicZio.scala index f2ddef3e68..7b0e4de9d5 100644 --- a/examples/src/main/scala/sttp/tapir/examples/security/ServerSecurityLogicZio.scala +++ b/examples/src/main/scala/sttp/tapir/examples/security/ServerSecurityLogicZio.scala @@ -1,8 +1,8 @@ // {cat=Security; effects=ZIO; server=ZIO HTTP}: Separating security and server logic, with a reusable base endpoint, accepting & refreshing credentials via cookies -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-zio-http-server:1.11.5 -//> using dep com.softwaremill.sttp.client3::async-http-client-backend-zio:3.9.8 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-zio-http-server:1.11.10 +//> using dep com.softwaremill.sttp.client3::async-http-client-backend-zio:3.10.1 package sttp.tapir.examples.security diff --git a/examples/src/main/scala/sttp/tapir/examples/security/basicAuthenticationPekkoServer.scala b/examples/src/main/scala/sttp/tapir/examples/security/basicAuthenticationPekkoServer.scala index 9832ed3f25..3dda89a7c9 100644 --- a/examples/src/main/scala/sttp/tapir/examples/security/basicAuthenticationPekkoServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/security/basicAuthenticationPekkoServer.scala @@ -1,8 +1,8 @@ // {cat=Security; effects=Future; server=Pekko HTTP}: HTTP basic authentication -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.11.5 -//> using dep com.softwaremill.sttp.client3::core:3.9.7 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.11.10 +//> using dep com.softwaremill.sttp.client3::core:3.9.8 package sttp.tapir.examples.security diff --git a/examples/src/main/scala/sttp/tapir/examples/security/corsInterceptorPekkoServer.scala b/examples/src/main/scala/sttp/tapir/examples/security/corsInterceptorPekkoServer.scala index e3682e3bb7..8fe721f98c 100644 --- a/examples/src/main/scala/sttp/tapir/examples/security/corsInterceptorPekkoServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/security/corsInterceptorPekkoServer.scala @@ -1,8 +1,8 @@ // {cat=Security; effects=Future; server=Pekko HTTP}: CORS interceptor -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.11.5 -//> using dep com.softwaremill.sttp.client3::core:3.9.7 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.11.10 +//> using dep com.softwaremill.sttp.client3::core:3.9.8 package sttp.tapir.examples.security diff --git a/examples/src/main/scala/sttp/tapir/examples/security/externalSecurityInterceptor.scala b/examples/src/main/scala/sttp/tapir/examples/security/externalSecurityInterceptor.scala index 1bc84186fd..2485c5a97a 100644 --- a/examples/src/main/scala/sttp/tapir/examples/security/externalSecurityInterceptor.scala +++ b/examples/src/main/scala/sttp/tapir/examples/security/externalSecurityInterceptor.scala @@ -1,8 +1,8 @@ // {cat=Security; effects=Future; server=Netty}: Interceptor verifying externally added security credentials -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server:1.11.5 -//> using dep com.softwaremill.sttp.client3::core:3.9.7 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server:1.11.10 +//> using dep com.softwaremill.sttp.client3::core:3.9.8 package sttp.tapir.examples.security diff --git a/examples/src/main/scala/sttp/tapir/examples/security/serverSecurityLogicPekko.scala b/examples/src/main/scala/sttp/tapir/examples/security/serverSecurityLogicPekko.scala index 439aabfe4e..c0e2e5f4f2 100644 --- a/examples/src/main/scala/sttp/tapir/examples/security/serverSecurityLogicPekko.scala +++ b/examples/src/main/scala/sttp/tapir/examples/security/serverSecurityLogicPekko.scala @@ -1,8 +1,8 @@ // {cat=Security; effects=Future; server=Pekko HTTP}: Separating security and server logic, with a reusable base endpoint -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.11.5 -//> using dep com.softwaremill.sttp.client3::core:3.9.7 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.11.10 +//> using dep com.softwaremill.sttp.client3::core:3.9.8 package sttp.tapir.examples.security diff --git a/examples/src/main/scala/sttp/tapir/examples/security/serverSecurityLogicRefreshCookiesPekko.scala b/examples/src/main/scala/sttp/tapir/examples/security/serverSecurityLogicRefreshCookiesPekko.scala index 1523d541ca..3cd93bd9ff 100644 --- a/examples/src/main/scala/sttp/tapir/examples/security/serverSecurityLogicRefreshCookiesPekko.scala +++ b/examples/src/main/scala/sttp/tapir/examples/security/serverSecurityLogicRefreshCookiesPekko.scala @@ -1,8 +1,8 @@ // {cat=Security; effects=Future; server=Pekko HTTP}: Separating security and server logic, with a reusable base endpoint, accepting & refreshing credentials via cookies -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.11.5 -//> using dep com.softwaremill.sttp.client3::core:3.9.7 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.11.10 +//> using dep com.softwaremill.sttp.client3::core:3.9.8 package sttp.tapir.examples.security diff --git a/examples/src/main/scala/sttp/tapir/examples/static_content/staticContentFromFilesNettyServer.scala b/examples/src/main/scala/sttp/tapir/examples/static_content/staticContentFromFilesNettyServer.scala index 38093b7bfe..f9ff3d79e1 100644 --- a/examples/src/main/scala/sttp/tapir/examples/static_content/staticContentFromFilesNettyServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/static_content/staticContentFromFilesNettyServer.scala @@ -1,8 +1,8 @@ // {cat=Static content; effects=Direct; server=Netty}: Serving static files from a directory -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-files:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-files:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.10 package sttp.tapir.examples.static_content diff --git a/examples/src/main/scala/sttp/tapir/examples/static_content/staticContentFromFilesPekkoServer.scala b/examples/src/main/scala/sttp/tapir/examples/static_content/staticContentFromFilesPekkoServer.scala index 7279919ef9..0c08a2be40 100644 --- a/examples/src/main/scala/sttp/tapir/examples/static_content/staticContentFromFilesPekkoServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/static_content/staticContentFromFilesPekkoServer.scala @@ -1,9 +1,9 @@ // {cat=Static content; effects=Future; server=Pekko HTTP}: Serving static files from a directory, with range requests -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-files:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.11.5 -//> using dep com.softwaremill.sttp.client3::core:3.9.7 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-files:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.11.10 +//> using dep com.softwaremill.sttp.client3::core:3.9.8 package sttp.tapir.examples.static_content diff --git a/examples/src/main/scala/sttp/tapir/examples/static_content/staticContentFromResourcesPekkoServer.scala b/examples/src/main/scala/sttp/tapir/examples/static_content/staticContentFromResourcesPekkoServer.scala index 1d45df2634..74a5c61539 100644 --- a/examples/src/main/scala/sttp/tapir/examples/static_content/staticContentFromResourcesPekkoServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/static_content/staticContentFromResourcesPekkoServer.scala @@ -1,8 +1,8 @@ // {cat=Static content; effects=Future; server=Pekko HTTP}: Serving static files from resources -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-files:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-files:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.11.10 package sttp.tapir.examples.static_content diff --git a/examples/src/main/scala/sttp/tapir/examples/static_content/staticContentSecurePekkoServer.scala b/examples/src/main/scala/sttp/tapir/examples/static_content/staticContentSecurePekkoServer.scala index c569fd1a29..6a30f396db 100644 --- a/examples/src/main/scala/sttp/tapir/examples/static_content/staticContentSecurePekkoServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/static_content/staticContentSecurePekkoServer.scala @@ -1,9 +1,9 @@ // {cat=Static content; effects=Future; server=Pekko HTTP}: Serving static files secured with a bearer token -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-files:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.11.5 -//> using dep com.softwaremill.sttp.client3::core:3.9.7 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-files:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.11.10 +//> using dep com.softwaremill.sttp.client3::core:3.9.8 package sttp.tapir.examples.static_content diff --git a/examples/src/main/scala/sttp/tapir/examples/status_code/statusCodeNettyServer.scala b/examples/src/main/scala/sttp/tapir/examples/status_code/statusCodeNettyServer.scala index 88d9e4dc78..3b18ef31b3 100644 --- a/examples/src/main/scala/sttp/tapir/examples/status_code/statusCodeNettyServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/status_code/statusCodeNettyServer.scala @@ -1,7 +1,7 @@ // {cat=Status code; effects=Direct; server=Netty}: Serving static files from a directory -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.10 package sttp.tapir.examples.status_code diff --git a/examples/src/main/scala/sttp/tapir/examples/streaming/ProxyHttp4sFs2Server.scala b/examples/src/main/scala/sttp/tapir/examples/streaming/ProxyHttp4sFs2Server.scala index 108385c9ec..4257115330 100644 --- a/examples/src/main/scala/sttp/tapir/examples/streaming/ProxyHttp4sFs2Server.scala +++ b/examples/src/main/scala/sttp/tapir/examples/streaming/ProxyHttp4sFs2Server.scala @@ -1,8 +1,8 @@ // {cat=Streaming; effects=cats-effect; server=http4s}: Proxy requests, handling bodies as fs2 streams -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.5 -//> using dep com.softwaremill.sttp.client3::fs2:3.9.7 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.10 +//> using dep com.softwaremill.sttp.client3::fs2:3.9.8 //> using dep org.http4s::http4s-blaze-server:0.23.16 package sttp.tapir.examples.streaming diff --git a/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingHttp4sFs2Server.scala b/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingHttp4sFs2Server.scala index f67f35dc06..834434b584 100644 --- a/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingHttp4sFs2Server.scala +++ b/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingHttp4sFs2Server.scala @@ -1,8 +1,8 @@ // {cat=Streaming; effects=cats-effect; server=http4s}: Stream response as an fs2 stream -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.5 -//> using dep com.softwaremill.sttp.client3::core:3.9.7 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.10 +//> using dep com.softwaremill.sttp.client3::core:3.9.8 //> using dep org.http4s::http4s-blaze-server:0.23.16 package sttp.tapir.examples.streaming diff --git a/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingHttp4sFs2ServerOrError.scala b/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingHttp4sFs2ServerOrError.scala index e3435d81f8..9f3c26e7ca 100644 --- a/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingHttp4sFs2ServerOrError.scala +++ b/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingHttp4sFs2ServerOrError.scala @@ -1,7 +1,7 @@ // {cat=Streaming; effects=cats-effect; server=http4s}: Respond with an fs2 stream, or with an error, represented as a failed effect in the business logic -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.10 //> using dep org.http4s::http4s-blaze-server:0.23.16 package sttp.tapir.examples.streaming diff --git a/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingNettyFs2Server.scala b/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingNettyFs2Server.scala index e19bb52d4a..836760a7f5 100644 --- a/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingNettyFs2Server.scala +++ b/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingNettyFs2Server.scala @@ -1,8 +1,8 @@ // {cat=Streaming; effects=cats-effect; server=Netty}: Stream response as an fs2 stream -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-cats:1.11.5 -//> using dep com.softwaremill.sttp.client3::core:3.9.7 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-cats:1.11.10 +//> using dep com.softwaremill.sttp.client3::core:3.9.8 package sttp.tapir.examples.streaming diff --git a/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingNettyZioServer.scala b/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingNettyZioServer.scala index ce0a57371e..c4847df25d 100644 --- a/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingNettyZioServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingNettyZioServer.scala @@ -1,8 +1,8 @@ // {cat=Streaming; effects=ZIO; server=Netty}: Stream response as a ZIO stream -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-zio:1.11.5 -//> using dep com.softwaremill.sttp.client3::core:3.9.7 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-zio:1.11.10 +//> using dep com.softwaremill.sttp.client3::core:3.9.8 package sttp.tapir.examples.streaming diff --git a/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingZioHttpServer.scala b/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingZioHttpServer.scala index 4d25ccf7f0..e2d6cd8d28 100644 --- a/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingZioHttpServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingZioHttpServer.scala @@ -1,8 +1,8 @@ // {cat=Streaming; effects=ZIO; server=ZIO HTTP}: Stream response as a ZIO stream -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-zio-http-server:1.11.5 -//> using dep com.softwaremill.sttp.client3::core:3.9.7 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-zio-http-server:1.11.10 +//> using dep com.softwaremill.sttp.client3::core:3.9.8 package sttp.tapir.examples.streaming diff --git a/examples/src/main/scala/sttp/tapir/examples/streaming/streamingPekkoServer.scala b/examples/src/main/scala/sttp/tapir/examples/streaming/streamingPekkoServer.scala index 7905f08b48..28817a4c2a 100644 --- a/examples/src/main/scala/sttp/tapir/examples/streaming/streamingPekkoServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/streaming/streamingPekkoServer.scala @@ -1,8 +1,8 @@ // {cat=Streaming; effects=Future; server=Pekko HTTP}: Stream response as a Pekko stream -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.11.5 -//> using dep com.softwaremill.sttp.client3::core:3.9.7 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.11.10 +//> using dep com.softwaremill.sttp.client3::core:3.9.8 package sttp.tapir.examples.streaming diff --git a/examples/src/main/scala/sttp/tapir/examples/testing/CatsServerStubInterpreter.scala b/examples/src/main/scala/sttp/tapir/examples/testing/CatsServerStubInterpreter.scala index 61cf0509eb..cb047a497e 100644 --- a/examples/src/main/scala/sttp/tapir/examples/testing/CatsServerStubInterpreter.scala +++ b/examples/src/main/scala/sttp/tapir/examples/testing/CatsServerStubInterpreter.scala @@ -1,9 +1,9 @@ // {cat=Testing; effects=cats-effect}: Test endpoints using the TapirStubInterpreter -//> using dep com.softwaremill.sttp.tapir::tapir-cats-effect:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-sttp-stub-server:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-cats:1.11.5 -//> using dep com.softwaremill.sttp.client3::core:3.9.7 +//> using dep com.softwaremill.sttp.tapir::tapir-cats-effect:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-sttp-stub-server:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-cats:1.11.10 +//> using dep com.softwaremill.sttp.client3::core:3.9.8 //> using dep org.scalatest::scalatest:3.2.19 package sttp.tapir.examples.testing diff --git a/examples/src/main/scala/sttp/tapir/examples/testing/PekkoServerStubInterpreter.scala b/examples/src/main/scala/sttp/tapir/examples/testing/PekkoServerStubInterpreter.scala index 856208ca1e..000415aea3 100644 --- a/examples/src/main/scala/sttp/tapir/examples/testing/PekkoServerStubInterpreter.scala +++ b/examples/src/main/scala/sttp/tapir/examples/testing/PekkoServerStubInterpreter.scala @@ -1,9 +1,9 @@ // {cat=Testing; effects=Future; server=Pekko HTTP}: Test endpoints using the TapirStubInterpreter -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-sttp-stub-server:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.11.5 -//> using dep com.softwaremill.sttp.client3::core:3.9.7 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-sttp-stub-server:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.11.10 +//> using dep com.softwaremill.sttp.client3::core:3.9.8 //> using dep org.scalatest::scalatest:3.2.19 package sttp.tapir.examples.testing diff --git a/examples/src/main/scala/sttp/tapir/examples/testing/SttpMockServerClientExample.scala b/examples/src/main/scala/sttp/tapir/examples/testing/SttpMockServerClientExample.scala index 6a55da2551..4846a922a3 100644 --- a/examples/src/main/scala/sttp/tapir/examples/testing/SttpMockServerClientExample.scala +++ b/examples/src/main/scala/sttp/tapir/examples/testing/SttpMockServerClientExample.scala @@ -1,9 +1,9 @@ // {cat=Testing; json=circe}: Test endpoints using the MockServer client -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.5 -//> using dep com.softwaremill.sttp.tapir::sttp-mock-server:1.11.5 -//> using dep com.softwaremill.sttp.client3::core:3.9.7 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.10 +//> using dep com.softwaremill.sttp.tapir::sttp-mock-server:1.11.10 +//> using dep com.softwaremill.sttp.client3::core:3.9.8 //> using dep org.mock-server:mockserver-netty:5.15.0 //> using dep org.scalatest::scalatest:3.2.19 diff --git a/examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketChatNettySyncServer.scala b/examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketChatNettySyncServer.scala index 8dd130a4b7..ead02cfedb 100644 --- a/examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketChatNettySyncServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketChatNettySyncServer.scala @@ -1,18 +1,19 @@ // {cat=WebSocket; effects=Direct; server=Netty}: A WebSocket chat across multiple clients connected to the same server -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.5 -//> using dep com.softwaremill.ox::core:0.4.0 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.10 package sttp.tapir.examples.websocket import ox.channels.{Actor, ActorRef, Channel, ChannelClosed, Default, DefaultResult, selectOrClosed} -import ox.{ExitCode, Ox, OxApp, fork, never, releaseAfterScope} +import ox.{ExitCode, Ox, OxApp, fork, never, releaseAfterScope, supervised} import sttp.tapir.* import sttp.tapir.CodecFormat.* import sttp.tapir.server.netty.sync.{NettySyncServer, OxStreams} import java.util.UUID +import ox.flow.Flow +import ox.flow.FlowEmit type ChatMemberId = UUID @@ -33,7 +34,7 @@ class ChatRoom: def incoming(message: Message): Unit = println(s"Broadcasting: ${message.v}") - members = members.flatMap { (id, member) => + members = members.flatMap: (id, member) => selectOrClosed(member.channel.sendClause(message), Default(())) match case member.channel.Sent() => Some((id, member)) case _: ChannelClosed => @@ -42,7 +43,6 @@ class ChatRoom: case DefaultResult(_) => println(s"Buffer for member $id full, not sending message") Some((id, member)) - } // @@ -53,27 +53,25 @@ val chatEndpoint = endpoint.get .in("chat") .out(webSocketBody[Message, TextPlain, Message, TextPlain](OxStreams)) -def chatProcessor(a: ActorRef[ChatRoom]): OxStreams.Pipe[Message, Message] = - incoming => { - val member = ChatMember.create +def chatProcessor(a: ActorRef[ChatRoom]): OxStreams.Pipe[Message, Message] = incoming => + // returning a flow which, when run, creates a scope to handle the incoming & outgoing messages + Flow.usingEmit: emit => + supervised: + val member = ChatMember.create - a.tell(_.connected(member)) + a.tell(_.connected(member)) - fork { - incoming.foreach { msg => - a.tell(_.incoming(msg)) - } - // all incoming messages are processed (= client closed), completing the outgoing channel as well - member.channel.done() - } + fork: + incoming.runForeach: msg => + a.tell(_.incoming(msg)) + // all incoming messages are processed (= client closed), completing the outgoing channel as well + member.channel.done() - // however the scope ends (client close or error), we need to notify the chat room - releaseAfterScope { - a.tell(_.disconnected(member)) - } + // however the scope ends (client close or error), we need to notify the chat room + releaseAfterScope: + a.tell(_.disconnected(member)) - member.channel - } + FlowEmit.channelToEmit(member.channel, emit) object WebSocketChatNettySyncServer extends OxApp: override def run(args: Vector[String])(using Ox): ExitCode = diff --git a/examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketHttp4sServer.scala index 3bf2ae2bf4..fcaa1fa5a5 100644 --- a/examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketHttp4sServer.scala @@ -1,11 +1,11 @@ // {cat=WebSocket; effects=cats-effect; server=http4s; json=circe; docs=AsyncAPI}: Describe and implement a WebSocket endpoint -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-asyncapi-docs:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-asyncapi-docs:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.10 //> using dep com.softwaremill.sttp.apispec::asyncapi-circe-yaml:0.10.0 -//> using dep com.softwaremill.sttp.client3::async-http-client-backend-fs2:3.9.8 +//> using dep com.softwaremill.sttp.client3::async-http-client-backend-fs2:3.10.1 //> using dep org.http4s::http4s-blaze-server:0.23.16 package sttp.tapir.examples.websocket diff --git a/examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketNettySyncServer.scala b/examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketNettySyncServer.scala index ba4386bffa..4170a3920d 100644 --- a/examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketNettySyncServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketNettySyncServer.scala @@ -1,20 +1,19 @@ // {cat=WebSocket; effects=Direct; server=Netty}: Describe and implement a WebSocket endpoint -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.10 package sttp.tapir.examples.websocket import ox.* import ox.channels.* +import ox.flow.Flow import sttp.capabilities.WebSockets import sttp.tapir.* import sttp.tapir.server.netty.sync.OxStreams import sttp.tapir.server.netty.sync.OxStreams.Pipe import sttp.tapir.server.netty.sync.NettySyncServer -import sttp.ws.WebSocketFrame -import java.util.concurrent.atomic.AtomicBoolean import scala.concurrent.duration.* object WebSocketNettySyncServer: @@ -22,33 +21,22 @@ object WebSocketNettySyncServer: val wsEndpoint = endpoint.get .in("ws") - .out( - webSocketBody[String, CodecFormat.TextPlain, String, CodecFormat.TextPlain](OxStreams) - .concatenateFragmentedFrames(false) // All these options are supported by tapir-netty - .ignorePong(true) - .autoPongOnPing(true) - .decodeCloseRequests(false) - .decodeCloseResponses(false) - .autoPing(Some((10.seconds, WebSocketFrame.Ping("ping-content".getBytes)))) - ) + .out(webSocketBody[String, CodecFormat.TextPlain, String, CodecFormat.TextPlain](OxStreams)) // Your processor transforming a stream of requests into a stream of responses val wsPipe: Pipe[String, String] = requestStream => requestStream.map(_.toUpperCase) // Alternative logic (not used here): requests and responses can be treated separately, for example to emit frames // to the client from another source. - val wsPipe2: Pipe[String, String] = { in => - val running = new AtomicBoolean(true) // TODO use https://github.com/softwaremill/ox/issues/209 once available - fork { - in.drain() // read and ignore requests - running.set(false) // stopping the responses - } + val wsPipe2: Pipe[String, String] = in => // emit periodic responses - Source.tick(1.second).takeWhile(_ => running.get()).map(_ => System.currentTimeMillis()).map(_.toString) - } + val responseFlow: Flow[String] = Flow.tick(1.second).map(_ => System.currentTimeMillis()).map(_.toString) + + // ignore whatever is sent by the client, but complete the stream once the client closes + in.drain().merge(responseFlow, propagateDoneLeft = true) // The WebSocket endpoint, builds the pipeline in serverLogicSuccess - val wsServerEndpoint = wsEndpoint.handleSuccess(_ => wsPipe2) + val wsServerEndpoint = wsEndpoint.handleSuccess(_ => wsPipe) // A regular /GET endpoint val helloWorldEndpoint = diff --git a/examples/src/main/scala/sttp/tapir/examples/websocket/webSocketPekkoServer.scala b/examples/src/main/scala/sttp/tapir/examples/websocket/webSocketPekkoServer.scala index adb1d15150..12a73edede 100644 --- a/examples/src/main/scala/sttp/tapir/examples/websocket/webSocketPekkoServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/websocket/webSocketPekkoServer.scala @@ -1,11 +1,11 @@ // {cat=WebSocket; effects=Future; server=Pekko HTTP}: Describe and implement a WebSocket endpoint -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-asyncapi-docs:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-asyncapi-docs:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.10 //> using dep com.softwaremill.sttp.apispec::asyncapi-circe-yaml:0.10.0 -//> using dep com.softwaremill.sttp.client3::pekko-http-backend:3.9.7 +//> using dep com.softwaremill.sttp.client3::pekko-http-backend:3.9.8 package sttp.tapir.examples.websocket diff --git a/generated-doc/out/adopters.md b/generated-doc/out/adopters.md index 6b86241a9a..c35a9403f2 100644 --- a/generated-doc/out/adopters.md +++ b/generated-doc/out/adopters.md @@ -41,3 +41,13 @@ Thank you! pits hootsuite +
+ematiq +fugo +budgetbakers +
+
+  +flo +  +
\ No newline at end of file diff --git a/generated-doc/out/adopters/budgetbakers.svg b/generated-doc/out/adopters/budgetbakers.svg new file mode 100644 index 0000000000..809689faea --- /dev/null +++ b/generated-doc/out/adopters/budgetbakers.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/generated-doc/out/adopters/ematiq.png b/generated-doc/out/adopters/ematiq.png new file mode 100644 index 0000000000..b15347a988 Binary files /dev/null and b/generated-doc/out/adopters/ematiq.png differ diff --git a/generated-doc/out/adopters/fugo.png b/generated-doc/out/adopters/fugo.png new file mode 100644 index 0000000000..d66b2cd43b Binary files /dev/null and b/generated-doc/out/adopters/fugo.png differ diff --git a/generated-doc/out/client/http4s.md b/generated-doc/out/client/http4s.md index 27767c71f4..039ee2e7bb 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.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-http4s-client" % "1.11.10" ``` 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 9ee650df8f..727776f2ea 100644 --- a/generated-doc/out/client/play.md +++ b/generated-doc/out/client/play.md @@ -6,13 +6,13 @@ See the [Play framework documentation](https://www.playframework.com/documentati For **Play 3.0**, add the dependency: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-play-client" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-play-client" % "1.11.10" ``` For **Play 2.9**, add ```scala -"com.softwaremill.sttp.tapir" %% "tapir-play29-client" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-play29-client" % "1.11.10" ``` instead. Furthermore, replace all uses of `sttp.capabilities.pekko.PekkoStreams` in the following code snippets with `sttp.capabilities.akka.AkkaStreams`. diff --git a/generated-doc/out/client/sttp.md b/generated-doc/out/client/sttp.md index a4e96a7a23..41d7ade2be 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.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-sttp-client" % "1.11.10" ``` To make requests using an endpoint definition using the [sttp client](https://github.com/softwaremill/sttp), import: @@ -55,7 +55,7 @@ A => I => Request[DecodeResult[Either[E, O]], R] A => I => F[DecodeResult[Either[E, O]]] ``` -See the [runnable example](https://github.com/softwaremill/tapir/blob/master/examples/src/main/scala/sttp/tapir/examples/BooksExample.scala) +See the [runnable example](https://github.com/softwaremill/tapir/blob/master/examples/src/main/scala/sttp/tapir/examples/booksExample.scala) for example usage. ## Web sockets @@ -101,7 +101,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.11.5" +"com.softwaremill.sttp.tapir" %%% "tapir-sttp-client" % "1.11.10" "io.github.cquiroz" %%% "scala-java-time" % "2.2.0" // implementations of java.time classes for Scala.JS ``` diff --git a/generated-doc/out/docs/asyncapi.md b/generated-doc/out/docs/asyncapi.md index 89372b2199..f4cbbf511c 100644 --- a/generated-doc/out/docs/asyncapi.md +++ b/generated-doc/out/docs/asyncapi.md @@ -3,7 +3,7 @@ To use, add the following dependencies: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-asyncapi-docs" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-asyncapi-docs" % "1.11.10" "com.softwaremill.sttp.apispec" %% "asyncapi-circe-yaml" % "..." // see https://github.com/softwaremill/sttp-apispec ``` diff --git a/generated-doc/out/docs/json-schema.md b/generated-doc/out/docs/json-schema.md index 0d25cece9d..9b40e9541c 100644 --- a/generated-doc/out/docs/json-schema.md +++ b/generated-doc/out/docs/json-schema.md @@ -3,7 +3,7 @@ You can conveniently generate JSON schema from Tapir schema, which can be derived from your Scala types. Use `TapirSchemaToJsonSchema`: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-apispec-docs" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-apispec-docs" % "1.11.10" ``` Schema generation can now be performed like in the following example: diff --git a/generated-doc/out/docs/openapi.md b/generated-doc/out/docs/openapi.md index 592d7f12d8..f3f40ecd56 100644 --- a/generated-doc/out/docs/openapi.md +++ b/generated-doc/out/docs/openapi.md @@ -13,7 +13,7 @@ these steps can be done separately, giving you complete control over the process 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.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-bundle" % "1.11.10" ``` Then, you can interpret a list of endpoints using `SwaggerInterpreter`. The result will be a list of file-serving @@ -55,7 +55,7 @@ for details. Similarly as above, you'll need the following dependency: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-redoc-bundle" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-redoc-bundle" % "1.11.10" ``` And the server endpoints can be generated using the `sttp.tapir.redoc.bundle.RedocInterpreter` class. @@ -65,7 +65,7 @@ And the server endpoints can be generated using the `sttp.tapir.redoc.bundle.Red To generate the docs in the OpenAPI yaml format, add the following dependencies: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-openapi-docs" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-openapi-docs" % "1.11.10" "com.softwaremill.sttp.apispec" %% "openapi-circe-yaml" % "..." // see https://github.com/softwaremill/sttp-apispec ``` @@ -133,7 +133,7 @@ For example, generating the OpenAPI 3.0.3 YAML string can be achieved by perform Firstly add dependencies: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-openapi-docs" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-openapi-docs" % "1.11.10" "com.softwaremill.sttp.apispec" %% "openapi-circe-yaml" % "..." // see https://github.com/softwaremill/sttp-apispec ``` @@ -163,12 +163,12 @@ The modules `tapir-swagger-ui` and `tapir-redoc` contain server endpoint definit 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.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-swagger-ui" % "1.11.10" ``` or `tapir-redoc`: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-redoc" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-redoc" % "1.11.10" ``` Then, you'll need to pass the server endpoints to your server interpreter. For example, using akka-http: diff --git a/generated-doc/out/endpoint/integrations.md b/generated-doc/out/endpoint/integrations.md index 0bfe39d4bd..f43d6ceaa9 100644 --- a/generated-doc/out/endpoint/integrations.md +++ b/generated-doc/out/endpoint/integrations.md @@ -12,7 +12,7 @@ The `tapir-cats` module contains additional instances for some [cats](https://ty datatypes as well as additional syntax: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-cats" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-cats" % "1.11.10" ``` - `import sttp.tapir.integ.cats.codec.*` - brings schema, validator and codec instances @@ -22,7 +22,7 @@ Additionally, the `tapir-cats-effect` module contains an implementation of the ` between the sttp-internal `MonadError` and the cats-effect `Sync` typeclass: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-cats-effect" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-cats-effect" % "1.11.10" ``` ## Refined integration @@ -31,7 +31,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.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-refined" % "1.11.10" ``` You'll need to extend the `sttp.tapir.codec.refined.TapirCodecRefined` @@ -52,7 +52,7 @@ If you use [iron](https://github.com/Iltotore/iron), the `tapir-iron` module wil validators for `T :| P` as long as a codec for `T` already exists: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-iron" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-iron" % "1.11.10" ``` The module is only available for Scala 3 since iron is not designed to work with Scala 2. @@ -145,7 +145,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.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-enumeratum" % "1.11.10" ``` Then, `import sttp.tapir.codec.enumeratum.*`, or extends the `sttp.tapir.codec.enumeratum.TapirCodecEnumeratum` trait. @@ -158,7 +158,7 @@ If you use [scala-newtype](https://github.com/estatico/scala-newtype), the `tapi schemas for 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.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-newtype" % "1.11.10" ``` Then, `import sttp.tapir.codec.newtype.*`, or extend the `sttp.tapir.codec.newtype.TapirCodecNewType` trait to bring the implicit values into scope. @@ -169,7 +169,7 @@ If you use [monix newtypes](https://github.com/monix/newtypes), the `tapir-monix schemas for types which extend `NewtypeWrapped` and `NewsubtypeWrapped` annotations as long as a codec and schema for its underlying value already exists: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-monix-newtype" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-monix-newtype" % "1.11.10" ``` Then, `import sttp.tapir.codec.monix.newtype.*`, or extend the `sttp.tapir.codec.monix.newtype.TapirCodecMonixNewType` trait to bring the implicit values into scope. @@ -180,7 +180,7 @@ If you use [ZIO Prelude Newtypes](https://zio.github.io/zio-prelude/docs/newtype schemas for types defined using `Newtype` and `Subtype` as long as a codec and a schema for the underlying type already exists: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-zio-prelude" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-zio-prelude" % "1.11.10" ``` Then, mix in `sttp.tapir.codec.zio.prelude.newtype.TapirNewtypeSupport` into your newtype to bring the implicit values into scope: @@ -219,7 +219,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.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-derevo" % "1.11.10" ``` Then you can derive schema for your ADT along with other typeclasses besides ADT declaration itself: diff --git a/generated-doc/out/endpoint/json.md b/generated-doc/out/endpoint/json.md index f57887e914..0bc9a65835 100644 --- a/generated-doc/out/endpoint/json.md +++ b/generated-doc/out/endpoint/json.md @@ -50,7 +50,7 @@ stringJsonBody.schema(implicitly[Schema[MyBody]].as[String]) To use [Circe](https://github.com/circe/circe), add the following dependency to your project: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-json-circe" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-json-circe" % "1.11.10" ``` Next, import the package (or extend the `TapirJsonCirce` trait, see [MyTapir](../other/mytapir.md)): @@ -122,7 +122,7 @@ Now the above JSON object will render as To use [µPickle](http://www.lihaoyi.com/upickle/) add the following dependency to your project: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-json-upickle" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-json-upickle" % "1.11.10" ``` Next, import the package (or extend the `TapirJsonuPickle` trait, see [MyTapir](../other/mytapir.md) and add `TapirJsonuPickle` not `TapirCirceJson`): @@ -156,13 +156,13 @@ For more examples, including making a custom encoder/decoder, see [TapirJsonuPic To use [Play JSON](https://github.com/playframework/play-json) for **Play 3.0**, add the following dependency to your project: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-json-play" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-json-play" % "1.11.10" ``` For **Play 2.9** use: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-json-play29" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-json-play29" % "1.11.10" ``` Next, import the package (or extend the `TapirJsonPlay` trait, see [MyTapir](../other/mytapir.md) and add `TapirJsonPlay` not `TapirCirceJson`): @@ -178,7 +178,7 @@ Play JSON requires `Reads` and `Writes` implicit values in scope for each type y 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.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-json-spray" % "1.11.10" ``` Next, import the package (or extend the `TapirJsonSpray` trait, see [MyTapir](../other/mytapir.md) and add `TapirJsonSpray` not `TapirCirceJson`): @@ -194,7 +194,7 @@ Spray JSON requires a `JsonFormat` implicit value in scope for each type you wan 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.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-json-tethys" % "1.11.10" ``` Next, import the package (or extend the `TapirJsonTethys` trait, see [MyTapir](../other/mytapir.md) and add `TapirJsonTethys` not `TapirCirceJson`): @@ -210,7 +210,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.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-jsoniter-scala" % "1.11.10" ``` Next, import the package (or extend the `TapirJsonJsoniter` trait, see [MyTapir](../other/mytapir.md) and add `TapirJsonJsoniter` not `TapirCirceJson`): @@ -226,7 +226,7 @@ Jsoniter Scala requires `JsonValueCodec` implicit value in scope for each type y To use [json4s](https://github.com/json4s/json4s) add the following dependencies to your project: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-json-json4s" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-json-json4s" % "1.11.10" ``` And one of the implementations: @@ -257,7 +257,7 @@ given Formats = org.json4s.jackson.Serialization.formats(NoTypeHints) 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.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-json-zio" % "1.11.10" ``` Next, import the package (or extend the `TapirJsonZio` trait, see [MyTapir](../other/mytapir.md) and add `TapirJsonZio` instead of `TapirCirceJson`): diff --git a/generated-doc/out/endpoint/pickler.md b/generated-doc/out/endpoint/pickler.md index 5146583b70..66e9959b77 100644 --- a/generated-doc/out/endpoint/pickler.md +++ b/generated-doc/out/endpoint/pickler.md @@ -9,7 +9,7 @@ In [other](json.md) tapir-JSON integrations, you have to keep the `Schema` (whic To use pickler, add the following dependency to your project: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-json-pickler" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-json-pickler" % "1.11.10" ``` Please note that it is available only for Scala 3 and Scala.JS 3. diff --git a/generated-doc/out/endpoint/static.md b/generated-doc/out/endpoint/static.md index f97d650e52..046e727ae9 100644 --- a/generated-doc/out/endpoint/static.md +++ b/generated-doc/out/endpoint/static.md @@ -11,7 +11,7 @@ the API documentation of the old static content API, switch documentation to an In order to use static content endpoints, add the module to your dependencies: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-files" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-files" % "1.11.10" ``` ## Files diff --git a/generated-doc/out/generator/sbt-openapi-codegen.md b/generated-doc/out/generator/sbt-openapi-codegen.md index e8ef0f555b..622db4eab1 100644 --- a/generated-doc/out/generator/sbt-openapi-codegen.md +++ b/generated-doc/out/generator/sbt-openapi-codegen.md @@ -9,7 +9,7 @@ This is a really early alpha implementation. Add the sbt plugin to the `project/plugins.sbt`: ```scala -addSbtPlugin("com.softwaremill.sttp.tapir" % "sbt-openapi-codegen" % "1.11.5") +addSbtPlugin("com.softwaremill.sttp.tapir" % "sbt-openapi-codegen" % "1.11.10") ``` Enable the plugin for your project in the `build.sbt`: diff --git a/generated-doc/out/how-tos/delimited-path-parameters.md b/generated-doc/out/how-tos/delimited-path-parameters.md new file mode 100644 index 0000000000..5feb23d73a --- /dev/null +++ b/generated-doc/out/how-tos/delimited-path-parameters.md @@ -0,0 +1,103 @@ +# Handling Delimited Path Parameters + +Tapir allows you to handle complex path parameters, such as lists of custom types separated by delimiters (e.g., commas). +This can be achieved using `Codec.delimited`, which facilitates the serialization and deserialization of delimited lists +within path segments. + +## Use Case + +Suppose you want to define an endpoint that accepts a list of names as a comma-separated path parameter. Each name should +adhere to a specific pattern (e.g., only uppercase letters). + +## Implementation Steps: + +### 1. Define the Custom Type and Validator +Start by defining your custom type and the associated validator to enforce the desired pattern. + +```scala +import sttp.tapir._ +import sttp.tapir.generic.auto._ +import sttp.tapir.Codec +import sttp.tapir.Validator +import sttp.tapir.CodecFormat.TextPlain +import sttp.tapir.model.Delimited + +case class Name(value: String) + +// Validator to ensure names consist of uppercase letters only +val nameValidator: Validator[String] = Validator.pattern("^[A-Z]+$") +``` + +### 2. Create Codecs for the Custom Type and Delimited List +Utilize `Codec.parsedString` for individual `Name` instances and `Codec.delimited` for handling the list. + +```scala +// Codec for single Name +given Codec[String, Name, TextPlain] = Codec.parsedString(Name.apply) + .validate(nameValidator.contramap(_.value)) + +// Codec for a list of Names, delimited by commas +given Codec[String, Delimited[",", Name], TextPlain] = Codec.delimited +``` + +### 3. Define the Endpoint with Delimited Path Parameter +Incorporate the delimited codec into your endpoint definition to handle the list of names in the path. + +```scala +import sttp.tapir._ +import sttp.tapir.generic.auto._ +import sttp.tapir.Codec +import sttp.tapir.Validator +import sttp.tapir.CodecFormat.TextPlain +import sttp.tapir.model.Delimited + +case class Name(value: String) + +// Validator to ensure names consist of uppercase letters only +val nameValidator: Validator[String] = Validator.pattern("^[A-Z]+$") + +// Codec for single Name +given Codec[String, Name, TextPlain] = Codec.parsedString(Name.apply) + .validate(nameValidator.contramap(_.value)) + +// Codec for a list of Names, delimited by commas +given Codec[String, Delimited[",", Name], TextPlain] = Codec.delimited + +val getUserEndpoint = + endpoint.get + .in("user" / path[Delimited[",", Name]]("id")) + .out(stringBody) +``` + +### 4. Generated OpenAPI Schema +When you generate the OpenAPI documentation for this endpoint, the schema for the `id` path parameter will +correctly reflect it as an array with the specified pattern for each item. + +```yaml +paths: + /user/{id}: + get: + operationId: getUserId + parameters: + - name: id + in: path + required: true + schema: + type: array + items: + type: string + pattern: ^[A-Z]+$ +``` + +## Explanation +- `Codec.parsedString`: Transforms a `String` into a custom type (`Name`) and vice versa. It also applies validation to + ensure each `Name` adheres to the specified pattern. +- `Codec.delimited`: Handles the serialization and deserialization of a delimited list (e.g., comma-separated) of the + custom type. By specifying `Delimited[",", Name]`, Tapir knows how to split and join the list based on the delimiter. +- Endpoint Definition: The `path[List[Name]]("id")` indicates that the id path parameter should be treated as a list of + `Name` objects, utilizing the previously defined codecs. + +## Validation +Validators play a crucial role in ensuring that each element within the delimited list meets the required criteria. In +this example, `nameValidator` ensures that each `Name` consists solely of uppercase letters. Tapir applies this validation +to each element in the list, providing robust input validation. \ No newline at end of file diff --git a/generated-doc/out/includes/examples_list.md b/generated-doc/out/includes/examples_list.md index cba3d66c36..3a4cc518ca 100644 --- a/generated-doc/out/includes/examples_list.md +++ b/generated-doc/out/includes/examples_list.md @@ -22,6 +22,7 @@ ## Custom types * [A demo of Tapir's capabilities using semi-auto derivation](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/custom_types/booksExampleSemiauto.scala) circe sttp3 Swagger UI Future Pekko HTTP +* [A query parameter which maps to a Scala 3 enum (enumeration)](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/custom_types/enumQueryParameter.scala) Direct Netty * [Handling comma-separated query parameters](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/custom_types/commaSeparatedQueryParameter.scala) Swagger UI Direct Netty * [Mapping a sealed trait hierarchy to JSON using a discriminator](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/custom_types/sealedTraitWithDiscriminator.scala) circe Swagger UI Direct Netty * [Supporting custom types, when used in query or path parameters, as well as part of JSON bodies](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/custom_types/EndpointWithCustomTypes.scala) circe diff --git a/generated-doc/out/index.md b/generated-doc/out/index.md index 5c520c636c..3a1676bd97 100644 --- a/generated-doc/out/index.md +++ b/generated-doc/out/index.md @@ -156,6 +156,7 @@ sttp is a family of Scala HTTP-related projects, and currently includes: examples external + how-tos/delimited-path-parameters .. toctree:: :maxdepth: 2 diff --git a/generated-doc/out/other/stability.md b/generated-doc/out/other/stability.md index 69a847375c..5e3f9f0038 100644 --- a/generated-doc/out/other/stability.md +++ b/generated-doc/out/other/stability.md @@ -6,6 +6,12 @@ The modules are categorised using the following levels: * **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 +The major version is increased when there are binary-incompatible changes in **stable** modules. + +The minor version is increased when there are significant new features in **stable** modules (keeping compatibility), or binary-incompatible changes in **stabilising** modules. + +The patch version is increased when there are binary-compatible changes in **stable** / **stabilising** modules, any changes in **exeperimental** modules, or when a new module is added (e.g. a new integration). + ## Main modules | Module | Level | diff --git a/generated-doc/out/quickstart.md b/generated-doc/out/quickstart.md index 3aef502639..9a35d76dde 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.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-core" % "1.11.10" ``` This will import only the core classes needed to create endpoint descriptions. To generate a server or a client, you diff --git a/generated-doc/out/server/akkahttp.md b/generated-doc/out/server/akkahttp.md index f9a6657842..857ee483dd 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.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-akka-http-server" % "1.11.10" ``` 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.11.5" exclude("com.typesafe.akka", "akka-stream_2.12") +"com.softwaremill.sttp.tapir" %% "tapir-akka-http-server" % "1.11.10" 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 d725b80835..347b266a14 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.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-armeria-server" % "1.11.10" ``` and import the object: @@ -71,7 +71,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.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-armeria-server-cats" % "1.11.10" ``` to use this interpreter with Cats Effect typeclasses. @@ -148,7 +148,7 @@ val tapirService = ArmeriaCatsServerInterpreter(dispatcher).toService(streamingR Add the following dependency ```scala -"com.softwaremill.sttp.tapir" %% "tapir-armeria-server-zio" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-armeria-server-zio" % "1.11.10" ``` to use this interpreter with ZIO. diff --git a/generated-doc/out/server/aws.md b/generated-doc/out/server/aws.md index d4c2141fa5..7fd0f0739c 100644 --- a/generated-doc/out/server/aws.md +++ b/generated-doc/out/server/aws.md @@ -30,7 +30,7 @@ These are corresponding classes for each of the supported runtime: To start using any of the above add the following dependency: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-aws-lambda" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-aws-lambda" % "1.11.10" ``` ## Deployment @@ -41,9 +41,9 @@ Tapir leverages ways of doing it provided by AWS, you can choose from: AWS SAM t You can start by adding one of the following dependencies to your project, and then follow examples: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-aws-sam" % "1.11.5" -"com.softwaremill.sttp.tapir" %% "tapir-aws-terraform" % "1.11.5" -"com.softwaremill.sttp.tapir" %% "tapir-aws-cdk" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-aws-sam" % "1.11.10" +"com.softwaremill.sttp.tapir" %% "tapir-aws-terraform" % "1.11.10" +"com.softwaremill.sttp.tapir" %% "tapir-aws-cdk" % "1.11.10" ``` ### Examples diff --git a/generated-doc/out/server/finatra.md b/generated-doc/out/server/finatra.md index fda839501e..a6442a24f8 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.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-finatra-server" % "1.11.10" ``` 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.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-finatra-server-cats" % "1.11.10" ``` and import the object: diff --git a/generated-doc/out/server/http4s.md b/generated-doc/out/server/http4s.md index a43a429234..cd1d2a5208 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.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-http4s-server" % "1.11.10" ``` and import the object: diff --git a/generated-doc/out/server/jdkhttp.md b/generated-doc/out/server/jdkhttp.md index 163549a152..9cc0bb2ce2 100644 --- a/generated-doc/out/server/jdkhttp.md +++ b/generated-doc/out/server/jdkhttp.md @@ -5,7 +5,7 @@ To expose endpoints using the (`com.sun.net.httpserver`), first add the following dependency: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-jdkhttp-server" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-jdkhttp-server" % "1.11.10" ``` Then, import the package: diff --git a/generated-doc/out/server/netty.md b/generated-doc/out/server/netty.md index 420ce624e7..51fcf0fa89 100644 --- a/generated-doc/out/server/netty.md +++ b/generated-doc/out/server/netty.md @@ -4,16 +4,16 @@ To expose an endpoint using a [Netty](https://netty.io)-based server, first add ```scala // if you are using Future or just exploring: -"com.softwaremill.sttp.tapir" %% "tapir-netty-server" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-netty-server" % "1.11.10" // if you want to use Java 21 Loom virtual threads in direct style: -"com.softwaremill.sttp.tapir" %% "tapir-netty-server-sync" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-netty-server-sync" % "1.11.10" // if you are using cats-effect: -"com.softwaremill.sttp.tapir" %% "tapir-netty-server-cats" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-netty-server-cats" % "1.11.10" // if you are using zio: -"com.softwaremill.sttp.tapir" %% "tapir-netty-server-zio" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-netty-server-zio" % "1.11.10" ``` Then, use: diff --git a/generated-doc/out/server/nima.md b/generated-doc/out/server/nima.md index a12fdb0549..84fb481625 100644 --- a/generated-doc/out/server/nima.md +++ b/generated-doc/out/server/nima.md @@ -8,7 +8,7 @@ To expose an endpoint as a [Helidon Níma](https://helidon.io/nima) server, firs dependency: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-nima-server" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-nima-server" % "1.11.10" ``` Loom-managed concurrency uses direct style instead of effect wrappers like `Future[T]` or `IO[T]`. Because of this, diff --git a/generated-doc/out/server/observability.md b/generated-doc/out/server/observability.md index c46b5b7797..c366dbe13d 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.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-prometheus-metrics" % "1.11.10" ``` `PrometheusMetrics` encapsulates `PrometheusReqistry` and `Metric` instances. It provides several ready to use metrics as @@ -128,7 +128,7 @@ val prometheusMetrics = PrometheusMetrics[Future]("tapir", PrometheusRegistry.de Add the following dependency: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-opentelemetry-metrics" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-opentelemetry-metrics" % "1.11.10" ``` OpenTelemetry metrics are vendor-agnostic and can be exported using one @@ -155,7 +155,7 @@ val metricsInterceptor = metrics.metricsInterceptor() // add to your server opti Add the following dependency: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-datadog-metrics" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-datadog-metrics" % "1.11.10" ``` Datadog metrics are sent as Datadog custom metrics through @@ -222,7 +222,7 @@ val datadogMetrics = DatadogMetrics.default[Future](statsdClient) Add the following dependency: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-zio-metrics" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-zio-metrics" % "1.11.10" ``` Metrics have been integrated into ZIO core in ZIO2. diff --git a/generated-doc/out/server/pekkohttp.md b/generated-doc/out/server/pekkohttp.md index 4e79606080..1fd8c1857f 100644 --- a/generated-doc/out/server/pekkohttp.md +++ b/generated-doc/out/server/pekkohttp.md @@ -4,14 +4,14 @@ To expose an endpoint as a [pekko-http](https://pekko.apache.org/docs/pekko-http dependency: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-pekko-http-server" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-pekko-http-server" % "1.11.10" ``` This will transitively pull some Pekko modules. If you want to force your own Pekko version, use sbt exclusion. Mind the Scala version in artifact name: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-pekko-http-server" % "1.11.5" exclude("org.apache.pekko", "pekko-stream_2.12") +"com.softwaremill.sttp.tapir" %% "tapir-pekko-http-server" % "1.11.10" exclude("org.apache.pekko", "pekko-stream_2.12") ``` Now import the object: diff --git a/generated-doc/out/server/play.md b/generated-doc/out/server/play.md index 815956d293..42cafb5cef 100644 --- a/generated-doc/out/server/play.md +++ b/generated-doc/out/server/play.md @@ -6,19 +6,19 @@ See the [Play framework documentation](https://www.playframework.com/documentati To expose an endpoint as a [play-server](https://www.playframework.com/), using **Play 2.9 with Akka**, add the following dependencies: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-play29-server" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-play29-server" % "1.11.10" ``` and (if you don't already depend on Play) ```scala -"org.playframework" %% "play-akka-http-server" % "2.9.5" +"org.playframework" %% "play-akka-http-server" % "2.9.6" ``` or ```scala -"org.playframework" %% "play-netty-server" % "2.9.5" +"org.playframework" %% "play-netty-server" % "2.9.6" ``` depending on whether you want to use netty or Akka based http-server under the hood. Please note that Play 2.9 server is available only for Scala 2.13. @@ -26,7 +26,7 @@ depending on whether you want to use netty or Akka based http-server under the h To expose an endpoint as a [play-server](https://www.playframework.com/), using **Play 3.0 with Pekko**, add the following dependencies: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-play-server" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-play-server" % "1.11.10" ``` 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 07712e68b8..18622d24d0 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.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-vertx-server" % "1.11.10" ``` to use this interpreter with `Future`. @@ -60,7 +60,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.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-vertx-server-cats" % "1.11.10" ``` to use this interpreter with Cats Effect typeclasses. @@ -140,7 +140,7 @@ val attach = VertxCatsServerInterpreter(dispatcher).route(streamedResponse.serve Add the following dependency ```scala -"com.softwaremill.sttp.tapir" %% "tapir-vertx-server-zio" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-vertx-server-zio" % "1.11.10" ``` to use this interpreter with ZIO. diff --git a/generated-doc/out/server/zio-http4s.md b/generated-doc/out/server/zio-http4s.md index 257b5536ef..63f712042f 100644 --- a/generated-doc/out/server/zio-http4s.md +++ b/generated-doc/out/server/zio-http4s.md @@ -8,13 +8,13 @@ The `*-zio` modules depend on ZIO 2.x. You'll need the following dependency for the `ZServerEndpoint` type alias and helper classes: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-zio" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-zio" % "1.11.10" ``` or just add the zio-http4s integration which already depends on `tapir-zio`: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-http4s-server-zio" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-http4s-server-zio" % "1.11.10" ``` Next, instead of the usual `import sttp.tapir.*`, you should import (or extend the `ZTapir` trait, see [MyTapir](../other/mytapir.md)): diff --git a/generated-doc/out/server/ziohttp.md b/generated-doc/out/server/ziohttp.md index ed5ce48bd9..2166ffedb6 100644 --- a/generated-doc/out/server/ziohttp.md +++ b/generated-doc/out/server/ziohttp.md @@ -8,13 +8,13 @@ The `*-zio` modules depend on ZIO 2.x. You'll need the following dependency for the `ZServerEndpoint` type alias and helper classes: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-zio" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-zio" % "1.11.10" ``` or just add the zio-http integration which already depends on `tapir-zio`: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-zio-http-server" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-zio-http-server" % "1.11.10" ``` Next, instead of the usual `import sttp.tapir.*`, you should import (or extend the `ZTapir` trait, see [MyTapir](../other/mytapir.md)): @@ -56,6 +56,14 @@ val countCharactersHttp: Routes[Any, Response] = ZioHttpInterpreter().toHttp(countCharactersEndpoint.zServerLogic(countCharacters)) ``` +```{note} +A single ZIO Http application can contain both Tapir-generated and ZIO-Http-native routes. However, because of the +routing implementation in ZIO Http, the shape of the paths that Tapir and other ZIO Http routes serve should not +overlap. The shape of the path includes exact path segments, single- and multi-wildcards. + +Such overlapping routes may cause incorrect 404 (Not Found) or 405 (Method Not Allowed) responses. +``` + ## Server logic When defining the business logic for an endpoint, the following methods are available, which replace the diff --git a/generated-doc/out/testing.md b/generated-doc/out/testing.md index 0cfc2c4e83..d929b3394f 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.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-sttp-stub-server" % "1.11.10" ``` Let's assume you are using the [pekko http](server/pekkohttp.md) interpreter. Given the following server endpoint: @@ -139,7 +139,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.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-sttp-stub-server" % "1.11.10" ``` And the following imports: @@ -194,7 +194,7 @@ with [mock-server](https://www.mock-server.com/) Add the following dependency: ```scala -"com.softwaremill.sttp.tapir" %% "sttp-mock-server" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "sttp-mock-server" % "1.11.10" ``` Imports: @@ -265,7 +265,7 @@ result == out To use, add the following dependency: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-testing" % "1.11.5" +"com.softwaremill.sttp.tapir" %% "tapir-testing" % "1.11.10" ``` ### Shadowed endpoints @@ -291,7 +291,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/generated-doc/out/tutorials/01_hello_world.md b/generated-doc/out/tutorials/01_hello_world.md index fb0471b708..3697396735 100644 --- a/generated-doc/out/tutorials/01_hello_world.md +++ b/generated-doc/out/tutorials/01_hello_world.md @@ -22,8 +22,8 @@ multiple servers, but we'll choose the simplest (and also one of the fastest!), available through the `tapir-netty-server-sync` module: ```scala -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.10 ``` ## Endpoint description @@ -43,8 +43,8 @@ Let's start by defining the method and path of our endpoint: {emphasize-lines="4-11"} ```scala -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.10 import sttp.tapir.* @@ -72,8 +72,8 @@ of requiring it to be a fixed value (a constant): {emphasize-lines="10"} ```scala -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.10 import sttp.tapir.* @@ -97,8 +97,8 @@ Finally, let's add an output to the endpoint. We'll return the response as a str {emphasize-lines="11"} ```scala -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.10 import sttp.tapir.* @@ -123,8 +123,8 @@ will be sent as a response: {emphasize-lines="12"} ```scala -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.10 import sttp.tapir.* @@ -152,8 +152,8 @@ example, we'll bind to `localhost` (which is the default), and to the port 8080: {emphasize-lines="5, 15-18"} ```scala -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.10 import sttp.tapir.* import sttp.tapir.server.netty.sync.NettySyncServer diff --git a/generated-doc/out/tutorials/02_openapi_docs.md b/generated-doc/out/tutorials/02_openapi_docs.md index 508674dfc0..5bf7b31ab3 100644 --- a/generated-doc/out/tutorials/02_openapi_docs.md +++ b/generated-doc/out/tutorials/02_openapi_docs.md @@ -17,16 +17,16 @@ use a bundle, which first interprets the provided tapir endpoints into OpenAPI a endpoints, which expose the UI together with the generated specification. We'll need to add a dependency: ```scala -//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.10 ``` We'll also define and expose two endpoints as an HTTP server, as described in the previous tutorial. Hence, our starting setup of `docs.scala` is as follows: ```scala -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.10 import sttp.tapir.* import sttp.tapir.server.netty.sync.NettySyncServer @@ -108,9 +108,9 @@ And that's almost all the code changes that we need to introduce! We only need t {emphasize-lines="3, 5, 8, 24-25, 29"} ```scala -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.10 import sttp.shared.Identity import sttp.tapir.* diff --git a/generated-doc/out/tutorials/03_json.md b/generated-doc/out/tutorials/03_json.md index 4af212c163..707c521c37 100644 --- a/generated-doc/out/tutorials/03_json.md +++ b/generated-doc/out/tutorials/03_json.md @@ -83,7 +83,7 @@ In our case, deriving the schemas will amount to adding a `... derives Schema` c {emphasize-lines="1, 7, 10, 12, 16"} ```scala -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 //> using dep com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros:2.30.1 import com.github.plokhotnyuk.jsoniter_scala.core.* // needed for `writeToString` @@ -127,10 +127,10 @@ how the `jsonBody[T]` method is used in the endpoint definition. We'll also expo {emphasize-lines="2-4, 10-15, 23-39"} ```scala -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-jsoniter-scala:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-jsoniter-scala:1.11.10 //> using dep com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros:2.30.1 import com.github.plokhotnyuk.jsoniter_scala.macros.* // needed for ... derives diff --git a/generated-doc/out/tutorials/04_errors.md b/generated-doc/out/tutorials/04_errors.md index 5a875b9d35..1ff3121aa0 100644 --- a/generated-doc/out/tutorials/04_errors.md +++ b/generated-doc/out/tutorials/04_errors.md @@ -40,8 +40,8 @@ schemas both for the `Result` and `Error` classes, to represent them properly in describing the endpoint: ```scala -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-jsoniter-scala:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-jsoniter-scala:1.11.10 //> using dep com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros:2.30.1 import com.github.plokhotnyuk.jsoniter_scala.macros.* @@ -76,10 +76,10 @@ We'll also add code to expose the endpoint as a server, along with its OpenAPI d {emphasize-lines="2-3, 11-13, 24-28, 30-36"} ```scala -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-jsoniter-scala:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-jsoniter-scala:1.11.10 //> using dep com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros:2.30.1 import com.github.plokhotnyuk.jsoniter_scala.macros.* @@ -152,10 +152,10 @@ you'll also get `ERROR` logs when unhandled exceptions happen: {emphasize-lines="6, 26"} ```scala -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-jsoniter-scala:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-jsoniter-scala:1.11.10 //> using dep com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros:2.30.1 //> using dep ch.qos.logback:logback-classic:1.5.6 diff --git a/generated-doc/out/tutorials/05_multiple_inputs_outputs.md b/generated-doc/out/tutorials/05_multiple_inputs_outputs.md index cf7fe95f8e..a170f77d8c 100644 --- a/generated-doc/out/tutorials/05_multiple_inputs_outputs.md +++ b/generated-doc/out/tutorials/05_multiple_inputs_outputs.md @@ -24,7 +24,7 @@ body, but additionally the hash of the result should be included in the `X-Resul Below is the endpoint description; we'll be editing the `multiple.scala` file: ```scala -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 import sttp.tapir.* @@ -61,8 +61,8 @@ The output tuple is then mapped to the response body & header: {emphasize-lines="5, 8-9, 18-29"} ```scala -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.10 import sttp.tapir.* import sttp.tapir.server.netty.sync.NettySyncServer @@ -147,8 +147,8 @@ The mapping functions are simple, but quite boring to write: {emphasize-lines="8, 17-18, 23-27"} ```scala -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.10 import sttp.tapir.* import sttp.tapir.server.netty.sync.NettySyncServer @@ -197,8 +197,8 @@ Here's the modified code using `.mapInTo`, which additionally maps outputs to th {emphasize-lines="9, 11-13, 19, 22"} ```scala -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.10 import sttp.tapir.* import sttp.tapir.server.netty.sync.NettySyncServer diff --git a/generated-doc/out/tutorials/06_error_variants.md b/generated-doc/out/tutorials/06_error_variants.md index 02baead739..a7ff6f517f 100644 --- a/generated-doc/out/tutorials/06_error_variants.md +++ b/generated-doc/out/tutorials/06_error_variants.md @@ -83,7 +83,7 @@ request serializing `AvatarSuccess.Redirect` instances, as Tapir knows nothing a an `EndpointOutput[String]`: ```scala -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 import sttp.model.{HeaderNames, StatusCode} import sttp.tapir.* @@ -106,7 +106,7 @@ this output to the `AvatarSuccess.Redirect` type using `.mapTo`, which we've lea {emphasize-lines="12-13"} ```scala -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 import sttp.model.{HeaderNames, StatusCode} import sttp.tapir.* @@ -138,7 +138,7 @@ each of which translates to a separate class. Our one-of successful output takes {emphasize-lines="13-16"} ```scala -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 import sttp.model.{HeaderNames, StatusCode} import sttp.tapir.* @@ -175,7 +175,7 @@ To fix this, we can use the `oneOfVariantSingletonMatcher` method. It takes a un value, to which the high-level output must be equal, for the variant to be chosen: ```scala -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 import sttp.model.{HeaderNames, StatusCode} import sttp.tapir.* @@ -197,9 +197,9 @@ val errorOutput: EndpointOutput[AvatarError] = oneOf( Equipped with `oneOf` outputs, we can now fully describe and test our endpoint: ```scala -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.10 import sttp.model.{HeaderNames, StatusCode} import sttp.tapir.* diff --git a/generated-doc/out/tutorials/07_cats_effect.md b/generated-doc/out/tutorials/07_cats_effect.md index 4b39bfe1fb..87838ff513 100644 --- a/generated-doc/out/tutorials/07_cats_effect.md +++ b/generated-doc/out/tutorials/07_cats_effect.md @@ -21,7 +21,7 @@ use. Hence, we'll start with the same basic endpoint description. We'll be editing the `cats-effect.scala` file: ```scala -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 import sttp.tapir.* @@ -56,8 +56,8 @@ parameter explicitly, using `.serverLogic[IO]` in our case: {emphasize-lines="2, 4, 12-14"} ```scala -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-cats:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-cats:1.11.10 import cats.effect.IO import sttp.tapir.* @@ -102,8 +102,8 @@ The conversion process is an almost-one-liner (if it wasn't for line length limi {emphasize-lines="2, 5, 7, 18-19"} ```scala -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.10 import cats.effect.IO import org.http4s.HttpRoutes @@ -130,8 +130,8 @@ standard code to start a server and handle requests until the application is int {emphasize-lines="3, 5, 7, 8, 12, 24-30"} ```scala -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.10 //> using dep org.http4s::http4s-blaze-server:0.23.16 import cats.effect.{ExitCode, IO, IOApp} @@ -192,9 +192,9 @@ the second step that we need to perform: {emphasize-lines="3, 7, 13, 27-32, 37"} ```scala -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.5 -//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.5 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.10 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.10 //> using dep org.http4s::http4s-blaze-server:0.23.16 import cats.effect.{ExitCode, IO, IOApp} diff --git a/integrations/iron/src/main/scala/sttp/iron/codec/iron/TapirCodecIron.scala b/integrations/iron/src/main/scala/sttp/iron/codec/iron/TapirCodecIron.scala index b040b38d87..e1b1fec702 100644 --- a/integrations/iron/src/main/scala/sttp/iron/codec/iron/TapirCodecIron.scala +++ b/integrations/iron/src/main/scala/sttp/iron/codec/iron/TapirCodecIron.scala @@ -209,7 +209,7 @@ trait TapirCodecIron extends DescriptionWitness with LowPriorityValidatorForPred } -private[iron] trait ValidatorForPredicate[Value, Predicate] { +trait ValidatorForPredicate[Value, Predicate] { def validator: Validator[Value] def makeErrors(value: Value, errorMessage: String): List[ValidationError[_]] lazy val containsMinSizePositive: Boolean = validator.asPrimitiveValidators.exists { @@ -218,12 +218,12 @@ private[iron] trait ValidatorForPredicate[Value, Predicate] { } } -private[iron] trait PrimitiveValidatorForPredicate[Value, Predicate] extends ValidatorForPredicate[Value, Predicate] { +trait PrimitiveValidatorForPredicate[Value, Predicate] extends ValidatorForPredicate[Value, Predicate] { def validator: Validator.Primitive[Value] def makeErrors(value: Value, errorMessage: String): List[ValidationError[_]] } -private[iron] object ValidatorForPredicate { +object ValidatorForPredicate { def fromPrimitiveValidator[Value, Predicate]( primitiveValidator: Validator.Primitive[Value] ): PrimitiveValidatorForPredicate[Value, Predicate] = @@ -235,7 +235,7 @@ private[iron] object ValidatorForPredicate { } // #3938: the two-level low-priority validators are needed because of implicit resolution changes in Scala 3.6 -private[iron] trait LowPriorityValidatorForPredicate extends LowPriorityValidatorForPredicate2 { +trait LowPriorityValidatorForPredicate extends LowPriorityValidatorForPredicate2 { inline given validatorForDescribedAnd[N, P](using id: IsDescription[P], diff --git a/integrations/iron/src/test/scala-3/com/example/RefinedString.scala b/integrations/iron/src/test/scala-3/com/example/RefinedString.scala new file mode 100644 index 0000000000..b820e9f486 --- /dev/null +++ b/integrations/iron/src/test/scala-3/com/example/RefinedString.scala @@ -0,0 +1,25 @@ +package com.example + +import io.github.iltotore.iron.* +import sttp.tapir.Validator +import sttp.tapir.codec.iron.PrimitiveValidatorForPredicate +import sttp.tapir.codec.iron.ValidatorForPredicate + +final class RefinedStringConstraint + +object RefinedStringConstraint { + + given Constraint[String, RefinedStringConstraint] with { + + override inline def test(value: String): Boolean = value.nonEmpty + + override inline def message: String = "Should not be empty" + } + + given PrimitiveValidatorForPredicate[String, RefinedStringConstraint] = + ValidatorForPredicate.fromPrimitiveValidator(Validator.pattern[String]("^.+")) +} + +opaque type RefinedString = String :| RefinedStringConstraint + +object RefinedString extends RefinedTypeOps[String, RefinedStringConstraint, RefinedString] diff --git a/integrations/iron/src/test/scala-3/sttp/iron/codec/iron/TapirCodecIronTestScala3.scala b/integrations/iron/src/test/scala-3/sttp/iron/codec/iron/TapirCodecIronTestScala3.scala index 4a9d7b1159..6af7435fea 100644 --- a/integrations/iron/src/test/scala-3/sttp/iron/codec/iron/TapirCodecIronTestScala3.scala +++ b/integrations/iron/src/test/scala-3/sttp/iron/codec/iron/TapirCodecIronTestScala3.scala @@ -18,6 +18,9 @@ import io.github.iltotore.iron.constraint.all.* import sttp.tapir.Validator import sttp.tapir.ValidationError +import com.example.RefinedString +import com.example.RefinedStringConstraint + class TapirCodecIronTestScala3 extends AnyFlatSpec with Matchers { val schema: Schema[Double :| Positive] = summon[Schema[Double :| Positive]] @@ -321,4 +324,11 @@ class TapirCodecIronTestScala3 extends AnyFlatSpec with Matchers { summon[Schema[NewtypeInt]] summon[Codec[String, NewtypeInt, TextPlain]] + "Instances for opaque refined type defined outside of source" should "be correctly derived" in: + summon[Schema[RefinedString]] + summon[Codec[String, RefinedString, TextPlain]] + + "Instance of validator for constraint defined outside of source" should "be correctly derived" in: + summon[PrimitiveValidatorForPredicate[String, RefinedStringConstraint]] + } diff --git a/json/pickler/src/main/scala/sttp/tapir/json/pickler/CreateDerivedEnumerationPickler.scala b/json/pickler/src/main/scala/sttp/tapir/json/pickler/CreateDerivedEnumerationPickler.scala index dd015f25ba..d1302c64aa 100644 --- a/json/pickler/src/main/scala/sttp/tapir/json/pickler/CreateDerivedEnumerationPickler.scala +++ b/json/pickler/src/main/scala/sttp/tapir/json/pickler/CreateDerivedEnumerationPickler.scala @@ -39,14 +39,14 @@ class CreateDerivedEnumerationPickler[T: ClassTag]( val tapirPickle = new TapirPickle[T] { override lazy val reader: Reader[T] = { val readersForPossibleValues: Seq[TaggedReader[T]] = - validator.possibleValues.zip(childReadWriters.map(_._1)).map { case (enumValue, reader) => - TaggedReader.Leaf[T](encode(enumValue).toString, reader.asInstanceOf[LeafWrapper[_]].r.asInstanceOf[Reader[T]]) + childReadWriters.map { case (enumValue, reader, _) => + TaggedReader.Leaf[T](encode(enumValue.asInstanceOf[T]).toString, reader.asInstanceOf[LeafWrapper[_]].r.asInstanceOf[Reader[T]]) } new TaggedReader.Node[T](readersForPossibleValues: _*) } override lazy val writer: Writer[T] = - new TaggedWriter.Node[T](childReadWriters.map(_._2.asInstanceOf[TaggedWriter[T]]): _*) { + new TaggedWriter.Node[T](childReadWriters.map(_._3.asInstanceOf[TaggedWriter[T]]): _*) { override def findWriterWithKey(v: Any): (String, String, ObjectWriter[T]) = val (tagKey, tagValue, writer) = super.findWriterWithKey(v) // Here our custom encoding transforms the value of a singleton object @@ -57,15 +57,17 @@ class CreateDerivedEnumerationPickler[T: ClassTag]( new Pickler[T](tapirPickle, schema) } - private inline def buildEnumerationReadWriters[T: ClassTag, Cases <: Tuple]: List[(Types#Reader[_], Types#Writer[_])] = + private inline def buildEnumerationReadWriters[T: ClassTag, Cases <: Tuple]: List[(Any, Types#Reader[_], Types#Writer[_])] = inline erasedValue[Cases] match { case _: (enumerationCase *: enumerationCasesTail) => - val processedHead = readWriterForEnumerationCase[enumerationCase] + val (reader, writer) = readWriterForEnumerationCase[enumerationCase] val processedTail = buildEnumerationReadWriters[T, enumerationCasesTail] - (processedHead +: processedTail) + ((productValue[enumerationCase], reader, writer) +: processedTail) case _: EmptyTuple.type => Nil } + private inline def productValue[E] = summonFrom { case m: Mirror.ProductOf[E] => m.fromProduct(EmptyTuple) } + /** Enumeration cases and case objects in an enumeration need special writers and readers, which are generated here, instead of being * taken from child picklers. For example, for enum Color and case values Red and Blue, a Writer should just use the object Red or Blue * and serialize it to "Red" or "Blue". If user needs to encode the singleton object using a custom function, this happens on a higher diff --git a/json/pickler/src/test/scala/sttp/tapir/json/pickler/Fixtures.scala b/json/pickler/src/test/scala/sttp/tapir/json/pickler/Fixtures.scala index 5878fe2622..167de23212 100644 --- a/json/pickler/src/test/scala/sttp/tapir/json/pickler/Fixtures.scala +++ b/json/pickler/src/test/scala/sttp/tapir/json/pickler/Fixtures.scala @@ -90,3 +90,7 @@ object Fixtures: sealed trait NotAllSealedVariant case object NotAllSealedVariantA extends NotAllSealedVariant case class NotAllSealedVariantB(innerField: Int) extends NotAllSealedVariant + + enum NotAlphabetical: + case Xyz + case Fgh diff --git a/json/pickler/src/test/scala/sttp/tapir/json/pickler/PicklerEnumTest.scala b/json/pickler/src/test/scala/sttp/tapir/json/pickler/PicklerEnumTest.scala index 0ebf26ce5d..ac3ef40c17 100644 --- a/json/pickler/src/test/scala/sttp/tapir/json/pickler/PicklerEnumTest.scala +++ b/json/pickler/src/test/scala/sttp/tapir/json/pickler/PicklerEnumTest.scala @@ -107,4 +107,18 @@ class PicklerEnumTest extends AnyFlatSpec with Matchers { 18 -> picklerMagenta )""") } + + it should "encode and decode an enum where the cases are not alphabetically sorted" in { + // given + import generic.auto.* // for Pickler auto-derivation + + // when + val testPickler = Pickler.derived[NotAlphabetical] + val codec = testPickler.toCodec + val encoded = codec.encode(NotAlphabetical.Xyz) + + // then + encoded shouldBe """"Xyz"""" + codec.decode(encoded) shouldBe Value(NotAlphabetical.Xyz) + } } diff --git a/metrics/opentelemetry-metrics/src/main/scala/sttp/tapir/server/metrics/opentelemetry/OpenTelemetryMetrics.scala b/metrics/opentelemetry-metrics/src/main/scala/sttp/tapir/server/metrics/opentelemetry/OpenTelemetryMetrics.scala index 9389c4c121..bea61f1e8d 100644 --- a/metrics/opentelemetry-metrics/src/main/scala/sttp/tapir/server/metrics/opentelemetry/OpenTelemetryMetrics.scala +++ b/metrics/opentelemetry-metrics/src/main/scala/sttp/tapir/server/metrics/opentelemetry/OpenTelemetryMetrics.scala @@ -15,15 +15,15 @@ import java.time.{Duration, Instant} case class OpenTelemetryMetrics[F[_]](meter: Meter, metrics: List[Metric[F, _]]) { /** Registers a `request_active{path, method}` up-down-counter (assuming default labels). */ - def addRequestsActive(labels: MetricLabels = MetricLabels.Default): OpenTelemetryMetrics[F] = + def addRequestsActive(labels: MetricLabels = OpenTelemetryAttributes): OpenTelemetryMetrics[F] = copy(metrics = metrics :+ requestActive(meter, labels)) /** Registers a `request_total{path, method, status}` counter (assuming default labels). */ - def addRequestsTotal(labels: MetricLabels = MetricLabels.Default): OpenTelemetryMetrics[F] = + def addRequestsTotal(labels: MetricLabels = OpenTelemetryAttributes): OpenTelemetryMetrics[F] = copy(metrics = metrics :+ requestTotal(meter, labels)) /** Registers a `request_duration_seconds{path, method, status, phase}` histogram (assuming default labels). */ - def addRequestsDuration(labels: MetricLabels = MetricLabels.Default): OpenTelemetryMetrics[F] = + def addRequestsDuration(labels: MetricLabels = OpenTelemetryAttributes): OpenTelemetryMetrics[F] = copy(metrics = metrics :+ requestDuration(meter, labels)) /** Registers a custom metric. */ @@ -36,6 +36,32 @@ case class OpenTelemetryMetrics[F[_]](meter: Meter, metrics: List[Metric[F, _]]) object OpenTelemetryMetrics { + /** Default labels for OpenTelemetry-compliant metrics, as recommended here: + * https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#http-server + * + * - `http.request.method` - HTTP request method (e.g., GET, POST). + * - `path` - The request path or route template. + * - `http.response.status_code` - HTTP response status code (200, 404, etc.). + */ + lazy val OpenTelemetryAttributes: MetricLabels = MetricLabels( + forRequest = List( + "http.request.method" -> { case (_, req) => req.method.method }, + "url.scheme" -> { case (_, req) => req.uri.scheme.getOrElse("unknown") }, + "path" -> { case (ep, _) => ep.showPathTemplate(showQueryParam = None) } + ), + forResponse = List( + "http.response.status_code" -> { + case Right(r) => Some(r.code.code.toString) + // Default to 500 for exceptions + case Left(_) => Some("500") + }, + "error.type" -> { + case Left(ex) => Some(ex.getClass.getName) // Exception class name for errors + case Right(_) => None + } + ) + ) + def apply[F[_]](meter: Meter): OpenTelemetryMetrics[F] = apply(meter, Nil) def apply[F[_]](otel: OpenTelemetry): OpenTelemetryMetrics[F] = apply(defaultMeter(otel), Nil) def apply[F[_]](otel: OpenTelemetry, metrics: List[Metric[F, _]]): OpenTelemetryMetrics[F] = apply(defaultMeter(otel), metrics) @@ -50,7 +76,7 @@ object OpenTelemetryMetrics { * measured separately up to the point where the headers are determined, and then once again when the whole response body is complete. */ def default[F[_]](otel: OpenTelemetry): OpenTelemetryMetrics[F] = - default(defaultMeter(otel), MetricLabels.Default) + default(defaultMeter(otel), OpenTelemetryAttributes) /** Registers default metrics (see other variants) using custom labels. */ def default[F[_]](otel: OpenTelemetry, labels: MetricLabels): OpenTelemetryMetrics[F] = default(defaultMeter(otel), labels) @@ -64,10 +90,10 @@ object OpenTelemetryMetrics { * Status is by default the status code class (1xx, 2xx, etc.), and phase can be either `headers` or `body` - request duration is * measured separately up to the point where the headers are determined, and then once again when the whole response body is complete. */ - def default[F[_]](meter: Meter): OpenTelemetryMetrics[F] = default(meter, MetricLabels.Default) + def default[F[_]](meter: Meter): OpenTelemetryMetrics[F] = default(meter, OpenTelemetryAttributes) /** Registers default metrics (see other variants) using custom labels. */ - def default[F[_]](meter: Meter, labels: MetricLabels = MetricLabels.Default): OpenTelemetryMetrics[F] = + def default[F[_]](meter: Meter, labels: MetricLabels = OpenTelemetryAttributes): OpenTelemetryMetrics[F] = OpenTelemetryMetrics( meter, List[Metric[F, _]]( @@ -80,7 +106,7 @@ object OpenTelemetryMetrics { def requestActive[F[_]](meter: Meter, labels: MetricLabels): Metric[F, LongUpDownCounter] = Metric[F, LongUpDownCounter]( meter - .upDownCounterBuilder("request_active") + .upDownCounterBuilder("http.server.active_requests") .setDescription("Active HTTP requests") .setUnit("1") .build(), @@ -97,7 +123,7 @@ object OpenTelemetryMetrics { def requestTotal[F[_]](meter: Meter, labels: MetricLabels): Metric[F, LongCounter] = Metric[F, LongCounter]( meter - .counterBuilder("request_total") + .counterBuilder("http.server.request.total") .setDescription("Total HTTP requests") .setUnit("1") .build(), @@ -108,6 +134,7 @@ object OpenTelemetryMetrics { m.eval { val otLabels = merge(asOpenTelemetryAttributes(labels, ep, req), asOpenTelemetryAttributes(labels, Right(res), None)) + counter.add(1, otLabels) } } @@ -125,9 +152,9 @@ object OpenTelemetryMetrics { def requestDuration[F[_]](meter: Meter, labels: MetricLabels): Metric[F, DoubleHistogram] = Metric[F, DoubleHistogram]( meter - .histogramBuilder("request_duration") + .histogramBuilder("http.server.request.duration") .setDescription("Duration of HTTP requests") - .setUnit("ms") + .setUnit("s") .build(), onRequest = (req, recorder, m) => m.eval { @@ -170,7 +197,10 @@ object OpenTelemetryMetrics { l.forRequest.foldLeft(Attributes.builder())((b, label) => { b.put(label._1, label._2(ep, req)) }).build() private def asOpenTelemetryAttributes(l: MetricLabels, res: Either[Throwable, ServerResponse[_]], phase: Option[String]): Attributes = { - val builder = l.forResponse.foldLeft(Attributes.builder())((b, label) => { b.put(label._1, label._2(res)) }) + val builder = Attributes.builder() + l.forResponse.foreach { case (key, valueFn) => + valueFn(res).foreach(value => builder.put(key, value)) + } phase.foreach(v => builder.put(l.forResponsePhase.name, v)) builder.build() } diff --git a/metrics/opentelemetry-metrics/src/test/scala/sttp/tapir/server/metrics/opentelemetry/OpenTelemetryMetricsTest.scala b/metrics/opentelemetry-metrics/src/test/scala/sttp/tapir/server/metrics/opentelemetry/OpenTelemetryMetricsTest.scala index fcc096a963..f0533fda8f 100644 --- a/metrics/opentelemetry-metrics/src/test/scala/sttp/tapir/server/metrics/opentelemetry/OpenTelemetryMetricsTest.scala +++ b/metrics/opentelemetry-metrics/src/test/scala/sttp/tapir/server/metrics/opentelemetry/OpenTelemetryMetricsTest.scala @@ -25,7 +25,7 @@ import scala.concurrent.Future class OpenTelemetryMetricsTest extends AnyFlatSpec with Matchers { - "default metrics" should "collect requests active" in { + "default metrics" should "collect http.server.active_requests" in { // given val reader = InMemoryMetricReader.create() val provider = SdkMeterProvider.builder().registerMetricReader(reader).build() @@ -51,17 +51,31 @@ class OpenTelemetryMetricsTest extends AnyFlatSpec with Matchers { // then val point = longSumData(reader).head - point.getAttributes shouldBe Attributes.of(AttributeKey.stringKey("method"), "GET", AttributeKey.stringKey("path"), "/person") + point.getAttributes shouldBe Attributes.of( + AttributeKey.stringKey("http.request.method"), + "GET", + AttributeKey.stringKey("path"), + "/person", + AttributeKey.stringKey("url.scheme"), + "http" + ) point.getValue shouldBe 1 ScalaFutures.whenReady(response, Timeout(Span(3, Seconds))) { _ => val point = longSumData(reader).head - point.getAttributes shouldBe Attributes.of(AttributeKey.stringKey("method"), "GET", AttributeKey.stringKey("path"), "/person") + point.getAttributes shouldBe Attributes.of( + AttributeKey.stringKey("http.request.method"), + "GET", + AttributeKey.stringKey("path"), + "/person", + AttributeKey.stringKey("url.scheme"), + "http" + ) point.getValue shouldBe 0 } } - "default metrics" should "collect requests total" in { + "default metrics" should "collect http.server.request.total" in { // given val reader = InMemoryMetricReader.create() val provider = SdkMeterProvider.builder().registerMetricReader(reader).build() @@ -87,29 +101,33 @@ class OpenTelemetryMetricsTest extends AnyFlatSpec with Matchers { .count { case dp if dp.getAttributes == Attributes.of( - AttributeKey.stringKey("method"), + AttributeKey.stringKey("http.request.method"), "GET", AttributeKey.stringKey("path"), "/person", - AttributeKey.stringKey("status"), - "2xx" + AttributeKey.stringKey("url.scheme"), + "http", + AttributeKey.stringKey("http.response.status_code"), + "200", ) && dp.getValue == 2 => true case dp if dp.getAttributes == Attributes.of( - AttributeKey.stringKey("method"), + AttributeKey.stringKey("http.request.method"), "GET", AttributeKey.stringKey("path"), "/person", - AttributeKey.stringKey("status"), - "4xx" + AttributeKey.stringKey("url.scheme"), + "http", + AttributeKey.stringKey("http.response.status_code"), + "400", ) && dp.getValue == 2 => true case _ => false } shouldBe 2 } - "default metrics" should "collect requests duration" in { + "default metrics" should "collect http.server.request.duration" in { // given val reader = InMemoryMetricReader.create() val provider = SdkMeterProvider.builder().registerMetricReader(reader).build() @@ -140,14 +158,16 @@ class OpenTelemetryMetricsTest extends AnyFlatSpec with Matchers { val point = reader.collectAllMetrics().asScala.head.getHistogramData.getPoints.asScala point.map(_.getAttributes) should contain( Attributes.of( - AttributeKey.stringKey("method"), + AttributeKey.stringKey("http.request.method"), "GET", AttributeKey.stringKey("path"), "/person", - AttributeKey.stringKey("status"), - "2xx", + AttributeKey.stringKey("http.response.status_code"), + "200", AttributeKey.stringKey("phase"), - "body" + "body", + AttributeKey.stringKey("url.scheme"), + "http", ) ) } @@ -197,12 +217,14 @@ class OpenTelemetryMetricsTest extends AnyFlatSpec with Matchers { // then val point = longSumData(reader).head point.getAttributes shouldBe Attributes.of( - AttributeKey.stringKey("method"), + AttributeKey.stringKey("http.request.method"), "GET", AttributeKey.stringKey("path"), "/person", - AttributeKey.stringKey("status"), - "5xx" + AttributeKey.stringKey("url.scheme"), + "http", + AttributeKey.stringKey("http.response.status_code"), + "500" ) point.getValue shouldBe 1 } diff --git a/metrics/zio-metrics/src/main/scala/sttp/tapir/server/metrics/zio/ZioMetrics.scala b/metrics/zio-metrics/src/main/scala/sttp/tapir/server/metrics/zio/ZioMetrics.scala index ff96d2c4ae..29d555440c 100644 --- a/metrics/zio-metrics/src/main/scala/sttp/tapir/server/metrics/zio/ZioMetrics.scala +++ b/metrics/zio-metrics/src/main/scala/sttp/tapir/server/metrics/zio/ZioMetrics.scala @@ -87,9 +87,12 @@ object ZioMetrics { /** Convert into zio metric labels */ private def asZioLabel(l: MetricLabels, res: Either[Throwable, ServerResponse[_]], phase: Option[String]): Set[MetricLabel] = { - l.forResponse.map(label => zio.metrics.MetricLabel(label._1, label._2(res))) ++ - phase.map(v => MetricLabel(l.forResponsePhase.name, v)) - }.toSet + val responseLabels = l.forResponse.map { case (key, valueFn) => + MetricLabel(key, valueFn(res).getOrElse("unknown")) + } + val phaseLabel = phase.map(v => MetricLabel(l.forResponsePhase.name, v)) + (responseLabels ++ phaseLabel).toSet + } /** Requests active metric collector. */ def requestActive[F[_]](namespace: String, labels: MetricLabels): Metric[F, Gauge[Long]] = { diff --git a/openapi-codegen/cli/src/main/scala/sttp/tapir/codegen/GenScala.scala b/openapi-codegen/cli/src/main/scala/sttp/tapir/codegen/GenScala.scala index 361167d58e..8de7b9cbd1 100644 --- a/openapi-codegen/cli/src/main/scala/sttp/tapir/codegen/GenScala.scala +++ b/openapi-codegen/cli/src/main/scala/sttp/tapir/codegen/GenScala.scala @@ -65,6 +65,9 @@ object GenScala { private val streamingImplementationOpt: Opts[Option[String]] = Opts.option[String]("streamingImplementation", "Capability to use for binary streams", "s").orNone + private val generateEndpointTypesOpt: Opts[Boolean] = + Opts.flag("generateEndpointTypes", "Whether to emit explicit type aliases for endpoint declarations", "e").orFalse + private val destDirOpt: Opts[File] = Opts .option[String]("destdir", "Destination directory", "d") @@ -88,7 +91,8 @@ object GenScala { jsonLibOpt, validateNonDiscriminatedOneOfsOpt, maxSchemasPerFileOpt, - streamingImplementationOpt + streamingImplementationOpt, + generateEndpointTypesOpt ) .mapN { case ( @@ -101,7 +105,8 @@ object GenScala { jsonLib, validateNonDiscriminatedOneOfs, maxSchemasPerFile, - streamingImplementation + streamingImplementation, + generateEndpointTypes ) => val objectName = maybeObjectName.getOrElse(DefaultObjectName) @@ -116,7 +121,8 @@ object GenScala { jsonLib.getOrElse("circe"), streamingImplementation.getOrElse("fs2"), validateNonDiscriminatedOneOfs, - maxSchemasPerFile.getOrElse(400) + maxSchemasPerFile.getOrElse(400), + generateEndpointTypes ) ) destFiles <- contents.toVector.traverse { case (fileName, content) => writeGeneratedFile(destDir, fileName, content) } diff --git a/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/BasicGenerator.scala b/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/BasicGenerator.scala index b7d21b9cdd..571fcc236b 100644 --- a/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/BasicGenerator.scala +++ b/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/BasicGenerator.scala @@ -25,6 +25,10 @@ object StreamingImplementation extends Enumeration { val Akka, FS2, Pekko, Zio = Value type StreamingImplementation = Value } +object EndpointCapabilites extends Enumeration { + val Akka, FS2, Nothing, Pekko, Zio = Value + type EndpointCapabilites = Value +} object BasicGenerator { @@ -40,7 +44,8 @@ object BasicGenerator { jsonSerdeLib: String, streamingImplementation: String, validateNonDiscriminatedOneOfs: Boolean, - maxSchemasPerFile: Int + maxSchemasPerFile: Int, + generateEndpointTypes: Boolean ): Map[String, String] = { val normalisedJsonLib = jsonSerdeLib.toLowerCase match { case "circe" => JsonSerdeLib.Circe @@ -65,7 +70,14 @@ object BasicGenerator { } val EndpointDefs(endpointsByTag, queryOrPathParamRefs, jsonParamRefs, enumsDefinedOnEndpointParams) = - endpointGenerator.endpointDefs(doc, useHeadTagForObjectNames, targetScala3, normalisedJsonLib, normalisedStreamingImplementation) + endpointGenerator.endpointDefs( + doc, + useHeadTagForObjectNames, + targetScala3, + normalisedJsonLib, + normalisedStreamingImplementation, + generateEndpointTypes + ) val GeneratedClassDefinitions(classDefns, jsonSerdes, schemas) = classGenerator .classDefs( diff --git a/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/EndpointGenerator.scala b/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/EndpointGenerator.scala index 7b388ffc93..a30d874efd 100644 --- a/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/EndpointGenerator.scala +++ b/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/EndpointGenerator.scala @@ -2,6 +2,8 @@ package sttp.tapir.codegen import io.circe.Json import sttp.tapir.codegen.BasicGenerator.{indent, mapSchemaSimpleTypeToType, strippedToCamelCase} import sttp.tapir.codegen.JsonSerdeLib.JsonSerdeLib +import sttp.tapir.codegen.EndpointCapabilites +import sttp.tapir.codegen.EndpointCapabilites.EndpointCapabilites import sttp.tapir.codegen.StreamingImplementation import sttp.tapir.codegen.StreamingImplementation.StreamingImplementation import sttp.tapir.codegen.openapi.models.OpenapiModels.{OpenapiDocument, OpenapiParameter, OpenapiPath, OpenapiRequestBody, OpenapiResponse} @@ -11,6 +13,7 @@ import sttp.tapir.codegen.openapi.models.OpenapiSchemaType.{ OpenapiSchemaBinary, OpenapiSchemaEnum, OpenapiSchemaMap, + OpenapiSchemaOneOf, OpenapiSchemaRef, OpenapiSchemaSimpleType, OpenapiSchemaString @@ -22,7 +25,18 @@ case class Location(path: String, method: String) { override def toString: String = s"${method.toUpperCase} ${path}" } -case class GeneratedEndpoint(name: String, definition: String, maybeLocalEnums: Option[String]) +case class EndpointTypes(security: Seq[String], in: Seq[String], err: Seq[String], out: Seq[String]) { + private def toType(types: Seq[String]) = types match { + case Nil => "Unit" + case t +: Nil => t + case seq => seq.mkString("(", ", ", ")") + } + def securityTypes = toType(security) + def inTypes = toType(in) + def errTypes = toType(err) + def outTypes = toType(out) +} +case class GeneratedEndpoint(name: String, definition: String, maybeLocalEnums: Option[String], types: EndpointTypes) case class GeneratedEndpointsForFile(maybeFileName: Option[String], generatedEndpoints: Seq[GeneratedEndpoint]) case class GeneratedEndpoints( @@ -54,22 +68,38 @@ class EndpointGenerator { private[codegen] def allEndpoints: String = "generatedEndpoints" + private def capabilityImpl(streamingImplementation: StreamingImplementation): String = streamingImplementation match { + case StreamingImplementation.Akka => "sttp.capabilities.akka.AkkaStreams" + case StreamingImplementation.FS2 => "sttp.capabilities.fs2.Fs2Streams[cats.effect.IO]" + case StreamingImplementation.Pekko => "sttp.capabilities.pekko.PekkoStreams" + case StreamingImplementation.Zio => "sttp.capabilities.zio.ZioStreams" + } + def endpointDefs( doc: OpenapiDocument, useHeadTagForObjectNames: Boolean, targetScala3: Boolean, jsonSerdeLib: JsonSerdeLib, - streamingImplementation: StreamingImplementation + streamingImplementation: StreamingImplementation, + generateEndpointTypes: Boolean ): EndpointDefs = { + val capabilities = capabilityImpl(streamingImplementation) val components = Option(doc.components).flatten val GeneratedEndpoints(endpointsByFile, queryOrPathParamRefs, jsonParamRefs, definesEnumQueryParam) = doc.paths - .map(generatedEndpoints(components, useHeadTagForObjectNames, targetScala3, jsonSerdeLib, streamingImplementation)) + .map(generatedEndpoints(components, useHeadTagForObjectNames, targetScala3, jsonSerdeLib, streamingImplementation, doc)) .foldLeft(GeneratedEndpoints(Nil, Set.empty, Set.empty, false))(_ merge _) val endpointDecls = endpointsByFile.map { case GeneratedEndpointsForFile(k, ge) => val definitions = ge - .map { case GeneratedEndpoint(name, definition, maybeEnums) => - s"""lazy val $name = + .map { case GeneratedEndpoint(name, definition, maybeEnums, types) => + val theCapabilities = if (definition.contains(".capabilities.")) capabilities else "Any" + val endpointTypeDecl = + if (generateEndpointTypes) + s"type ${name.capitalize}Endpoint = Endpoint[${types.securityTypes}, ${types.inTypes}, ${types.errTypes}, ${types.outTypes}, $theCapabilities]\n" + else "" + + val maybeType = if (generateEndpointTypes) s": ${name.capitalize}Endpoint" else "" + s"""${endpointTypeDecl}lazy val $name$maybeType = |${indent(2)(definition)}${maybeEnums.fold("")("\n" + _)} |""".stripMargin } @@ -89,7 +119,8 @@ class EndpointGenerator { useHeadTagForObjectNames: Boolean, targetScala3: Boolean, jsonSerdeLib: JsonSerdeLib, - streamingImplementation: StreamingImplementation + streamingImplementation: StreamingImplementation, + doc: OpenapiDocument )(p: OpenapiPath): GeneratedEndpoints = { val parameters = components.map(_.parameters).getOrElse(Map.empty) val securitySchemes = components.map(_.securitySchemes).getOrElse(Map.empty) @@ -111,15 +142,19 @@ class EndpointGenerator { } val name = strippedToCamelCase(m.operationId.getOrElse(m.methodType + p.url.capitalize)) - val (inParams, maybeLocalEnums) = + val (pathDecl, pathTypes) = urlMapper(p.url, m.resolvedParameters) + val (securityDecl, securityTypes) = security(securitySchemes, m.security) + val (inParams, maybeLocalEnums, inTypes) = ins(m.resolvedParameters, m.requestBody, name, targetScala3, jsonSerdeLib, streamingImplementation) + val (outDecl, outTypes, errTypes) = outs(m.responses, streamingImplementation, doc, targetScala3) + val allTypes = EndpointTypes(securityTypes.toSeq, pathTypes ++ inTypes, errTypes.toSeq, outTypes.toSeq) val definition = s"""|endpoint | .${m.methodType} - | ${urlMapper(p.url, m.resolvedParameters)} - |${indent(2)(security(securitySchemes, m.security))} + | $pathDecl + |${indent(2)(securityDecl)} |${indent(2)(inParams)} - |${indent(2)(outs(m.responses, streamingImplementation))} + |${indent(2)(outDecl)} |${indent(2)(tags(m.tags))} |$attributeString |""".stripMargin.linesIterator.filterNot(_.trim.isEmpty).mkString("\n") @@ -148,7 +183,7 @@ class EndpointGenerator { } .toSet ( - (maybeTargetFileName, GeneratedEndpoint(name, definition, maybeLocalEnums)), + (maybeTargetFileName, GeneratedEndpoint(name, definition, maybeLocalEnums, allTypes)), (queryOrPathParamRefs, jsonParamRefs), maybeLocalEnums.isDefined ) @@ -167,26 +202,30 @@ class EndpointGenerator { ) } - private def urlMapper(url: String, parameters: Seq[OpenapiParameter])(implicit location: Location): String = { + private def urlMapper(url: String, parameters: Seq[OpenapiParameter])(implicit location: Location): (String, Seq[String]) = { // .in(("books" / path[String]("genre") / path[Int]("year")).mapTo[BooksFromYear]) - val inPath = url.split('/').filter(_.nonEmpty) map { segment => - if (segment.startsWith("{")) { - val name = segment.drop(1).dropRight(1) - val param = parameters.find(_.name == name) - param.fold(bail(s"URLParam $name not found!")) { p => - p.schema match { - case st: OpenapiSchemaSimpleType => - val (t, _) = mapSchemaSimpleTypeToType(st) - val desc = p.description.fold("")(d => s""".description("$d")""") - s"""path[$t]("$name")$desc""" - case _ => bail("Can't create non-simple params to url yet") + val (inPath, tpes) = url + .split('/') + .filter(_.nonEmpty) + .map { segment => + if (segment.startsWith("{")) { + val name = segment.drop(1).dropRight(1) + val param = parameters.find(_.name == name) + param.fold(bail(s"URLParam $name not found!")) { p => + p.schema match { + case st: OpenapiSchemaSimpleType => + val (t, _) = mapSchemaSimpleTypeToType(st) + val desc = p.description.fold("")(d => s""".description("$d")""") + s"""path[$t]("$name")$desc""" -> Some(t) + case _ => bail("Can't create non-simple params to url yet") + } } + } else { + '"' + segment + '"' -> None } - } else { - '"' + segment + '"' } - } - ".in((" + inPath.mkString(" / ") + "))" + .unzip + ".in((" + inPath.mkString(" / ") + "))" -> tpes.toSeq.flatten } private def security(securitySchemes: Map[String, OpenapiSecuritySchemeType], security: Seq[Seq[String]])(implicit location: Location) = { @@ -195,16 +234,16 @@ class EndpointGenerator { security.headOption .flatMap(_.headOption) - .fold("") { schemeName => + .fold("" -> Option.empty[String]) { schemeName => securitySchemes.get(schemeName) match { case Some(OpenapiSecuritySchemeType.OpenapiSecuritySchemeBearerType) => - ".securityIn(auth.bearer[String]())" + ".securityIn(auth.bearer[String]())" -> Some("String") case Some(OpenapiSecuritySchemeType.OpenapiSecuritySchemeBasicType) => - ".securityIn(auth.basic[UsernamePassword]())" + ".securityIn(auth.basic[UsernamePassword]())" -> Some("String") case Some(OpenapiSecuritySchemeType.OpenapiSecuritySchemeApiKeyType(in, name)) => - s""".securityIn(auth.apiKey($in[String]("$name")))""" + s""".securityIn(auth.apiKey($in[String]("$name")))""" -> Some("String") case None => bail(s"Unknown security scheme $schemeName!") @@ -219,7 +258,7 @@ class EndpointGenerator { targetScala3: Boolean, jsonSerdeLib: JsonSerdeLib, streamingImplementation: StreamingImplementation - )(implicit location: Location): (String, Option[String]) = { + )(implicit location: Location): (String, Option[String], Seq[String]) = { def getEnumParamDefn(param: OpenapiParameter, e: OpenapiSchemaEnum, isArray: Boolean) = { val enumName = endpointName.capitalize + strippedToCamelCase(param.name).capitalize val enumParamRefs = if (param.in == "query" || param.in == "path") Set(enumName) else Set.empty[String] @@ -237,14 +276,20 @@ class EndpointGenerator { // 'exploded' params have no distinction between an empty list and an absent value, so don't wrap in 'Option' for them val noOptionWrapper = required || (isArray && param.isExploded) val req = if (noOptionWrapper) tpe else s"Option[$tpe]" + val outType = (isArray, noOptionWrapper) match { + case (true, true) => s"List[$enumName]" + case (true, false) => s"Option[List[$enumName]]" + case (false, true) => enumName + case (false, false) => s"Option[$enumName]" + } def mapToList = if (!isArray) "" else if (noOptionWrapper) s".map(_.values)($arrayType(_))" else s".map(_.map(_.values))(_.map($arrayType(_)))" val desc = param.description.map(d => JavaEscape.escapeString(d)).fold("")(d => s""".description("$d")""") - s""".in(${param.in}[$req]("${param.name}")$mapToList$desc)""" -> Some(enumDefn) + (s""".in(${param.in}[$req]("${param.name}")$mapToList$desc)""", Some(enumDefn), outType) } // .in(query[Limit]("limit").description("Maximum number of books to retrieve")) // .in(header[AuthToken]("X-Auth-Token")) - val (params, maybeEnumDefns) = parameters + val (params, maybeEnumDefns, inTypes) = parameters .filter(_.in != "path") .map { param => param.schema match { @@ -252,7 +297,7 @@ class EndpointGenerator { val (t, _) = mapSchemaSimpleTypeToType(st) val req = if (param.required.getOrElse(true)) t else s"Option[$t]" val desc = param.description.map(d => JavaEscape.escapeString(d)).fold("")(d => s""".description("$d")""") - s""".in(${param.in}[$req]("${param.name}")$desc)""" -> None + (s""".in(${param.in}[$req]("${param.name}")$desc)""", None, req) case OpenapiSchemaArray(st: OpenapiSchemaSimpleType, _) => val (t, _) = mapSchemaSimpleTypeToType(st) val arrayType = if (param.isExploded) "ExplodedValues" else "CommaSeparatedValues" @@ -263,25 +308,32 @@ class EndpointGenerator { val req = if (noOptionWrapper) arr else s"Option[$arr]" def mapToList = if (noOptionWrapper) s".map(_.values)($arrayType(_))" else s".map(_.map(_.values))(_.map($arrayType(_)))" val desc = param.description.map(d => JavaEscape.escapeString(d)).fold("")(d => s""".description("$d")""") - s""".in(${param.in}[$req]("${param.name}")$mapToList$desc)""" -> None + (s""".in(${param.in}[$req]("${param.name}")$mapToList$desc)""", None, req) case e @ OpenapiSchemaEnum(_, _, _) => getEnumParamDefn(param, e, isArray = false) case OpenapiSchemaArray(e: OpenapiSchemaEnum, _) => getEnumParamDefn(param, e, isArray = true) case x => bail(s"Can't create non-simple params to input - found $x") } } - .unzip + .unzip3 - val rqBody = requestBody.flatMap { b => + val (rqBody, maybeReqType) = requestBody.flatMap { b => if (b.content.isEmpty) None else if (b.content.size != 1) bail(s"We can handle only one requestBody content! Saw ${b.content.map(_.contentType)}") - else Some(s".in(${contentTypeMapper(b.content.head.contentType, b.content.head.schema, streamingImplementation, b.required)})") - } + else { + val (decl, tpe) = contentTypeMapper(b.content.head.contentType, b.content.head.schema, streamingImplementation, b.required) + Some(s".in($decl)" -> tpe) + } + }.unzip - (params ++ rqBody).mkString("\n") -> maybeEnumDefns.foldLeft(Option.empty[String]) { - case (acc, None) => acc - case (None, Some(nxt)) => Some(nxt.mkString("\n")) - case (Some(acc), Some(nxt)) => Some(acc + "\n" + nxt.mkString("\n")) - } + ( + (params ++ rqBody).mkString("\n"), + maybeEnumDefns.foldLeft(Option.empty[String]) { + case (acc, None) => acc + case (None, Some(nxt)) => Some(nxt.mkString("\n")) + case (Some(acc), Some(nxt)) => Some(acc + "\n" + nxt.mkString("\n")) + }, + inTypes ++ maybeReqType + ) } private def tags(openapiTags: Option[Seq[String]]): String = { @@ -305,7 +357,14 @@ class EndpointGenerator { // treats redirects as ok private val okStatus = """([23]\d\d)""".r private val errorStatus = """([45]\d\d)""".r - private def outs(responses: Seq[OpenapiResponse], streamingImplementation: StreamingImplementation)(implicit location: Location) = { + private def outs( + responses: Seq[OpenapiResponse], + streamingImplementation: StreamingImplementation, + doc: OpenapiDocument, + targetScala3: Boolean + )(implicit + location: Location + ) = { // .errorOut(stringBody) // .out(jsonBody[List[Book]]) @@ -320,44 +379,83 @@ class EndpointGenerator { case _ => bail("We can handle only one return content!") } } - def bodyFmt(resp: OpenapiResponse): String = { + def bodyFmt(resp: OpenapiResponse): (String, Option[String]) = { val d = s""".description("${JavaEscape.escapeString(resp.description)}")""" resp.content match { - case Nil => "" + case Nil => "" -> None case content +: Nil => - s"${contentTypeMapper(content.contentType, content.schema, streamingImplementation)}$d" + val (decl, tpe) = contentTypeMapper(content.contentType, content.schema, streamingImplementation) + s"$decl$d" -> Some(tpe) } } - def mappedGroup(group: Seq[OpenapiResponse]) = group match { - case Nil => None + def mappedGroup(group: Seq[OpenapiResponse]): (Option[String], Option[String]) = group match { + case Nil => None -> None case resp +: Nil => resp.content match { case Nil => val d = s""".description("${JavaEscape.escapeString(resp.description)}")""" - resp.code match { - case "200" | "default" => None - case okStatus(s) => Some(s"statusCode(sttp.model.StatusCode($s))$d") - case errorStatus(s) => Some(s"statusCode(sttp.model.StatusCode($s))$d") - } + ( + resp.code match { + case "200" | "default" => None + case okStatus(s) => Some(s"statusCode(sttp.model.StatusCode($s))$d") + case errorStatus(s) => Some(s"statusCode(sttp.model.StatusCode($s))$d") + }, + None + ) case _ => + val (decl, tpe) = bodyFmt(resp) Some(resp.code match { - case "200" | "default" => s"${bodyFmt(resp)}" - case okStatus(s) => s"${bodyFmt(resp)}.and(statusCode(sttp.model.StatusCode($s)))" - case errorStatus(s) => s"${bodyFmt(resp)}.and(statusCode(sttp.model.StatusCode($s)))" - }) + case "200" | "default" => decl + case okStatus(s) => s"$decl.and(statusCode(sttp.model.StatusCode($s)))" + case errorStatus(s) => s"$decl.and(statusCode(sttp.model.StatusCode($s)))" + }) -> tpe } case many => if (many.map(_.code).distinct.size != many.size) bail("Cannot construct schema for multiple responses with same status code") - val oneOfs = many.map { m => + val (oneOfs, types) = many.map { m => + val (decl, tpe) = bodyFmt(m) val code = if (m.code == "default") "400" else m.code - s"oneOfVariant(sttp.model.StatusCode(${code}), ${bodyFmt(m)})" - } - Some(s"oneOf(${oneOfs.mkString(", ")})") + s"oneOfVariant(sttp.model.StatusCode(${code}), $decl)" -> tpe + }.unzip + val parentMap = doc.components.toSeq + .flatMap(_.schemas) + .collect { case (k, v: OpenapiSchemaOneOf) => + v.types.map { + case r: OpenapiSchemaRef => r.stripped -> k + case x: OpenapiSchemaSimpleType => mapSchemaSimpleTypeToType(x)._1 -> k + case x => bail(s"Unexpected oneOf child type $x") + } + } + .flatten + .groupBy(_._1) + .map { case (k, vs) => k -> vs.map(_._2) } + .toMap + val allElemTypes = many + .flatMap(_.content.map(_.schema)) + .map { + case r: OpenapiSchemaRef => r.stripped + case x: OpenapiSchemaSimpleType => mapSchemaSimpleTypeToType(x)._1 + case x => bail(s"Unexpected oneOf elem type $x") + } + .distinct + val commmonType = + if (allElemTypes.size == 1) allElemTypes.head + else + allElemTypes.map { s => parentMap.getOrElse(s, Nil).toSet }.reduce(_ intersect _) match { + case s if s.isEmpty && targetScala3 => types.mkString(" | ") + case s if s.isEmpty => "Any" + case s if targetScala3 => s.mkString(" & ") + case s => s.mkString(" with ") + } + Some(s"oneOf[$commmonType](${oneOfs.mkString(", ")})") -> Some(commmonType) } - val mappedOuts = mappedGroup(outs).map(s => s".out($s)") - val mappedErrorOuts = mappedGroup(errorOuts).map(s => s".errorOut($s)") - Seq(mappedErrorOuts, mappedOuts).flatten.mkString("\n") + val (outDecls, outTypes) = mappedGroup(outs) + val mappedOuts = outDecls.map(s => s".out($s)") + val (errDecls, errTypes) = mappedGroup(errorOuts) + val mappedErrorOuts = errDecls.map(s => s".errorOut($s)") + + (Seq(mappedErrorOuts, mappedOuts).flatten.mkString("\n"), outTypes, errTypes) } private def contentTypeMapper( @@ -365,10 +463,10 @@ class EndpointGenerator { schema: OpenapiSchemaType, streamingImplementation: StreamingImplementation, required: Boolean = true - )(implicit location: Location) = { + )(implicit location: Location): (String, String) = { contentType match { case "text/plain" => - "stringBody" + "stringBody" -> "String" case "application/json" => val outT = schema match { case st: OpenapiSchemaSimpleType => @@ -383,27 +481,22 @@ class EndpointGenerator { case x => bail(s"Can't create non-simple or array params as output (found $x)") } val req = if (required) outT else s"Option[$outT]" - s"jsonBody[$req]" + s"jsonBody[$req]" -> req case "multipart/form-data" => schema match { case _: OpenapiSchemaBinary => - "multipartBody" + "multipartBody" -> " Seq[Part[Array[Byte]]]" case schemaRef: OpenapiSchemaRef => val (t, _) = mapSchemaSimpleTypeToType(schemaRef, multipartForm = true) - s"multipartBody[$t]" + s"multipartBody[$t]" -> t case x => bail(s"$contentType only supports schema ref or binary. Found $x") } case "application/octet-stream" => - val capability = streamingImplementation match { - case StreamingImplementation.Akka => "sttp.capabilities.akka.AkkaStreams" - case StreamingImplementation.FS2 => "sttp.capabilities.fs2.Fs2Streams[cats.effect.IO]" - case StreamingImplementation.Pekko => "sttp.capabilities.pekko.PekkoStreams" - case StreamingImplementation.Zio => "sttp.capabilities.zio.ZioStreams" - } + val capability = capabilityImpl(streamingImplementation) schema match { case _: OpenapiSchemaString => - s"streamTextBody($capability)(CodecFormat.OctetStream())" + s"streamTextBody($capability)(CodecFormat.OctetStream())" -> s"$capability.BinaryStream" case schema => val outT = schema match { case st: OpenapiSchemaSimpleType => @@ -417,7 +510,7 @@ class EndpointGenerator { s"Map[String, $t]" case x => bail(s"Can't create this param as output (found $x)") } - s"streamBody($capability)(Schema.binary[$outT], CodecFormat.OctetStream())" + s"streamBody($capability)(Schema.binary[$outT], CodecFormat.OctetStream())" -> s"$capability.BinaryStream" } case x => bail(s"Not all content types supported! Found $x") diff --git a/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/SchemaGenerator.scala b/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/SchemaGenerator.scala index 7d6bc95434..d245e88a2c 100644 --- a/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/SchemaGenerator.scala +++ b/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/SchemaGenerator.scala @@ -123,7 +123,9 @@ object SchemaGenerator { res ++= nextLayers.toSeq.sortBy(_.head._1) acquired ++= nextLayers.flatMap(_.map(_._1)).toSet if (initialSet.nonEmpty && nextLayers.isEmpty) - throw new IllegalStateException("Cannot order layers until mutually-recursive references have been resolved.") + throw new IllegalStateException( + s"Cannot order layers until mutually-recursive references have been resolved. Unable to find all dependencies for ${initialSet.flatMap(_.map(_._1))}" + ) } res.map(_.map { case (k, v, _) => k -> v }) diff --git a/openapi-codegen/core/src/test/scala/sttp/tapir/codegen/BasicGeneratorSpec.scala b/openapi-codegen/core/src/test/scala/sttp/tapir/codegen/BasicGeneratorSpec.scala index a4f334339b..d1b5d61501 100644 --- a/openapi-codegen/core/src/test/scala/sttp/tapir/codegen/BasicGeneratorSpec.scala +++ b/openapi-codegen/core/src/test/scala/sttp/tapir/codegen/BasicGeneratorSpec.scala @@ -18,7 +18,8 @@ class BasicGeneratorSpec extends CompileCheckTestBase { jsonSerdeLib = jsonSerdeLib, validateNonDiscriminatedOneOfs = true, maxSchemasPerFile = 400, - streamingImplementation = "fs2" + streamingImplementation = "fs2", + generateEndpointTypes = false ) } def gen( diff --git a/openapi-codegen/core/src/test/scala/sttp/tapir/codegen/ClassDefinitionGeneratorSpec.scala b/openapi-codegen/core/src/test/scala/sttp/tapir/codegen/ClassDefinitionGeneratorSpec.scala index b73060d8a7..de41c97a6c 100644 --- a/openapi-codegen/core/src/test/scala/sttp/tapir/codegen/ClassDefinitionGeneratorSpec.scala +++ b/openapi-codegen/core/src/test/scala/sttp/tapir/codegen/ClassDefinitionGeneratorSpec.scala @@ -396,7 +396,8 @@ class ClassDefinitionGeneratorSpec extends CompileCheckTestBase { useHeadTagForObjectNames = false, targetScala3 = false, jsonSerdeLib = JsonSerdeLib.Circe, - streamingImplementation = StreamingImplementation.FS2 + streamingImplementation = StreamingImplementation.FS2, + generateEndpointTypes = false ) .endpointDecls(None) } diff --git a/openapi-codegen/core/src/test/scala/sttp/tapir/codegen/EndpointGeneratorSpec.scala b/openapi-codegen/core/src/test/scala/sttp/tapir/codegen/EndpointGeneratorSpec.scala index 453ea55dab..c0f199ef31 100644 --- a/openapi-codegen/core/src/test/scala/sttp/tapir/codegen/EndpointGeneratorSpec.scala +++ b/openapi-codegen/core/src/test/scala/sttp/tapir/codegen/EndpointGeneratorSpec.scala @@ -68,7 +68,8 @@ class EndpointGeneratorSpec extends CompileCheckTestBase { useHeadTagForObjectNames = false, targetScala3 = false, jsonSerdeLib = JsonSerdeLib.Circe, - streamingImplementation = StreamingImplementation.FS2 + streamingImplementation = StreamingImplementation.FS2, + generateEndpointTypes = false ) .endpointDecls(None) generatedCode should include("val getTestAsdId =") @@ -153,7 +154,8 @@ class EndpointGeneratorSpec extends CompileCheckTestBase { useHeadTagForObjectNames = false, targetScala3 = false, jsonSerdeLib = JsonSerdeLib.Circe, - streamingImplementation = StreamingImplementation.FS2 + streamingImplementation = StreamingImplementation.FS2, + generateEndpointTypes = false ) .endpointDecls(None) shouldCompile () } @@ -205,7 +207,8 @@ class EndpointGeneratorSpec extends CompileCheckTestBase { useHeadTagForObjectNames = false, targetScala3 = false, jsonSerdeLib = JsonSerdeLib.Circe, - streamingImplementation = StreamingImplementation.FS2 + streamingImplementation = StreamingImplementation.FS2, + generateEndpointTypes = false ) .endpointDecls(None) generatedCode should include( @@ -272,7 +275,8 @@ class EndpointGeneratorSpec extends CompileCheckTestBase { jsonSerdeLib = "circe", validateNonDiscriminatedOneOfs = true, maxSchemasPerFile = 400, - streamingImplementation = "fs2" + streamingImplementation = "fs2", + generateEndpointTypes = false )("TapirGeneratedEndpoints") generatedCode should include( """file: sttp.model.Part[java.io.File]""" @@ -294,7 +298,8 @@ class EndpointGeneratorSpec extends CompileCheckTestBase { jsonSerdeLib = "circe", validateNonDiscriminatedOneOfs = true, maxSchemasPerFile = 400, - streamingImplementation = "fs2" + streamingImplementation = "fs2", + generateEndpointTypes = false )("TapirGeneratedEndpoints") generatedCode shouldCompile () val expectedAttrDecls = Seq( diff --git a/openapi-codegen/sbt-plugin/src/main/scala/sttp/tapir/sbt/OpenapiCodegenKeys.scala b/openapi-codegen/sbt-plugin/src/main/scala/sttp/tapir/sbt/OpenapiCodegenKeys.scala index ba00d8a34e..510f5b8540 100644 --- a/openapi-codegen/sbt-plugin/src/main/scala/sttp/tapir/sbt/OpenapiCodegenKeys.scala +++ b/openapi-codegen/sbt-plugin/src/main/scala/sttp/tapir/sbt/OpenapiCodegenKeys.scala @@ -11,6 +11,7 @@ case class OpenApiConfiguration( streamingImplementation: String, validateNonDiscriminatedOneOfs: Boolean, maxSchemasPerFile: Int, + generateEndpointTypes: Boolean, additionalPackages: List[(String, File)] ) @@ -27,6 +28,7 @@ trait OpenapiCodegenKeys { lazy val openapiMaxSchemasPerFile = settingKey[Int]("Maximum number of schemas to generate for a single file") lazy val openapiAdditionalPackages = settingKey[List[(String, File)]]("Addition package -> spec mappings to generate.") lazy val openapiStreamingImplementation = settingKey[String]("Implementation for streamTextBody. Supports: akka, fs2, pekko, zio.") + lazy val openapiGenerateEndpointTypes = settingKey[Boolean]("Whether to emit explicit types for endpoint denfs") lazy val openapiOpenApiConfiguration = settingKey[OpenApiConfiguration]("Aggregation of other settings. Manually set value will be disregarded.") diff --git a/openapi-codegen/sbt-plugin/src/main/scala/sttp/tapir/sbt/OpenapiCodegenPlugin.scala b/openapi-codegen/sbt-plugin/src/main/scala/sttp/tapir/sbt/OpenapiCodegenPlugin.scala index 5f086617e1..16ebcb092b 100644 --- a/openapi-codegen/sbt-plugin/src/main/scala/sttp/tapir/sbt/OpenapiCodegenPlugin.scala +++ b/openapi-codegen/sbt-plugin/src/main/scala/sttp/tapir/sbt/OpenapiCodegenPlugin.scala @@ -32,6 +32,7 @@ object OpenapiCodegenPlugin extends AutoPlugin { openapiStreamingImplementation.value, openapiValidateNonDiscriminatedOneOfs.value, openapiMaxSchemasPerFile.value, + openapiGenerateEndpointTypes.value, openapiAdditionalPackages.value ) def openapiCodegenDefaultSettings: Seq[Setting[_]] = Seq( @@ -44,6 +45,7 @@ object OpenapiCodegenPlugin extends AutoPlugin { openapiMaxSchemasPerFile := 400, openapiAdditionalPackages := Nil, openapiStreamingImplementation := "fs2", + openapiGenerateEndpointTypes := false, standardParamSetting ) @@ -73,6 +75,7 @@ object OpenapiCodegenPlugin extends AutoPlugin { c.streamingImplementation, c.validateNonDiscriminatedOneOfs, c.maxSchemasPerFile, + c.generateEndpointTypes, srcDir, taskStreams.cacheDirectory, sv.startsWith("3"), diff --git a/openapi-codegen/sbt-plugin/src/main/scala/sttp/tapir/sbt/OpenapiCodegenTask.scala b/openapi-codegen/sbt-plugin/src/main/scala/sttp/tapir/sbt/OpenapiCodegenTask.scala index e689e3a09e..ea07a0a226 100644 --- a/openapi-codegen/sbt-plugin/src/main/scala/sttp/tapir/sbt/OpenapiCodegenTask.scala +++ b/openapi-codegen/sbt-plugin/src/main/scala/sttp/tapir/sbt/OpenapiCodegenTask.scala @@ -14,6 +14,7 @@ case class OpenapiCodegenTask( streamingImplementation: String, validateNonDiscriminatedOneOfs: Boolean, maxSchemasPerFile: Int, + generateEndpointTypes: Boolean, dir: File, cacheDir: File, targetScala3: Boolean, @@ -59,7 +60,8 @@ case class OpenapiCodegenTask( jsonSerdeLib, streamingImplementation, validateNonDiscriminatedOneOfs, - maxSchemasPerFile + maxSchemasPerFile, + generateEndpointTypes ) .map { case (objectName, fileBody) => val file = directory / s"$objectName.scala" diff --git a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/caching/project/build.properties b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/caching/project/build.properties index 0b699c3052..e88a0d817d 100644 --- a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/caching/project/build.properties +++ b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/caching/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.2 +sbt.version=1.10.6 diff --git a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/minimal/project/build.properties b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/minimal/project/build.properties index 0b699c3052..e88a0d817d 100644 --- a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/minimal/project/build.properties +++ b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/minimal/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.2 +sbt.version=1.10.6 diff --git a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip/Expected.scala.txt b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip/Expected.scala.txt index 4b2c350770..98e8d347f2 100644 --- a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip/Expected.scala.txt +++ b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip/Expected.scala.txt @@ -44,7 +44,6 @@ object TapirGeneratedEndpoints { support.mapDecode(l => DecodeResult.Value(ExplodedValues(l)))(_.values) } - case class EnumExtraParamSupport[T <: enumeratum.EnumEntry](enumName: String, T: enumeratum.Enum[T]) extends ExtraParamSupport[T] { // Case-insensitive mapping def decode(s: String): sttp.tapir.DecodeResult[T] = @@ -63,9 +62,16 @@ object TapirGeneratedEndpoints { } def extraCodecSupport[T <: enumeratum.EnumEntry](enumName: String, T: enumeratum.Enum[T]): ExtraParamSupport[T] = EnumExtraParamSupport(enumName, T) + sealed trait Error sealed trait ADTWithoutDiscriminator sealed trait ADTWithDiscriminator sealed trait ADTWithDiscriminatorNoMapping + case class SimpleError ( + message: String + ) extends Error + case class NotFoundError ( + reason: String + ) extends Error case class SubtypeWithoutD1 ( s: String, i: Option[Int] = None, @@ -119,34 +125,39 @@ object TapirGeneratedEndpoints { - lazy val getBinaryTest = + type GetBinaryTestEndpoint = Endpoint[Unit, Unit, Unit, sttp.capabilities.pekko.PekkoStreams.BinaryStream, sttp.capabilities.pekko.PekkoStreams] + lazy val getBinaryTest: GetBinaryTestEndpoint = endpoint .get .in(("binary" / "test")) .out(streamBody(sttp.capabilities.pekko.PekkoStreams)(Schema.binary[Array[Byte]], CodecFormat.OctetStream()).description("Response CSV body")) - lazy val postBinaryTest = + type PostBinaryTestEndpoint = Endpoint[Unit, sttp.capabilities.pekko.PekkoStreams.BinaryStream, Unit, String, sttp.capabilities.pekko.PekkoStreams] + lazy val postBinaryTest: PostBinaryTestEndpoint = endpoint .post .in(("binary" / "test")) .in(streamBody(sttp.capabilities.pekko.PekkoStreams)(Schema.binary[Array[Byte]], CodecFormat.OctetStream())) .out(jsonBody[String].description("successful operation")) - lazy val putAdtTest = + type PutAdtTestEndpoint = Endpoint[Unit, ADTWithoutDiscriminator, Unit, ADTWithoutDiscriminator, Any] + lazy val putAdtTest: PutAdtTestEndpoint = endpoint .put .in(("adt" / "test")) .in(jsonBody[ADTWithoutDiscriminator]) .out(jsonBody[ADTWithoutDiscriminator].description("successful operation")) - lazy val postAdtTest = + type PostAdtTestEndpoint = Endpoint[Unit, ADTWithDiscriminatorNoMapping, Unit, ADTWithDiscriminator, Any] + lazy val postAdtTest: PostAdtTestEndpoint = endpoint .post .in(("adt" / "test")) .in(jsonBody[ADTWithDiscriminatorNoMapping]) .out(jsonBody[ADTWithDiscriminator].description("successful operation")) - lazy val postInlineEnumTest = + type PostInlineEnumTestEndpoint = Endpoint[Unit, (PostInlineEnumTestQueryEnum, Option[PostInlineEnumTestQueryOptEnum], List[PostInlineEnumTestQuerySeqEnum], Option[List[PostInlineEnumTestQueryOptSeqEnum]], ObjectWithInlineEnum), Unit, Unit, Any] + lazy val postInlineEnumTest: PostInlineEnumTestEndpoint = endpoint .post .in(("inline" / "enum" / "test")) @@ -197,7 +208,14 @@ object TapirGeneratedEndpoints { extraCodecSupport[PostInlineEnumTestQueryOptSeqEnum]("PostInlineEnumTestQueryOptSeqEnum", PostInlineEnumTestQueryOptSeqEnum) } + type GetOneofErrorTestEndpoint = Endpoint[Unit, Unit, Error, Unit, Any] + lazy val getOneofErrorTest: GetOneofErrorTestEndpoint = + endpoint + .get + .in(("oneof" / "error" / "test")) + .errorOut(oneOf[Error](oneOfVariant(sttp.model.StatusCode(404), jsonBody[NotFoundError].description("Not found")), oneOfVariant(sttp.model.StatusCode(400), jsonBody[SimpleError].description("Not found")))) + .out(statusCode(sttp.model.StatusCode(204)).description("No response")) - lazy val generatedEndpoints = List(getBinaryTest, postBinaryTest, putAdtTest, postAdtTest, postInlineEnumTest) + lazy val generatedEndpoints = List(getBinaryTest, postBinaryTest, putAdtTest, postAdtTest, postInlineEnumTest, getOneofErrorTest) } diff --git a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip/ExpectedJsonSerdes.scala.txt b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip/ExpectedJsonSerdes.scala.txt index eff2439305..9013ccbdfc 100644 --- a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip/ExpectedJsonSerdes.scala.txt +++ b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip/ExpectedJsonSerdes.scala.txt @@ -16,6 +16,10 @@ object TapirGeneratedEndpointsJsonSerdes { } } yield res } + implicit lazy val simpleErrorJsonDecoder: io.circe.Decoder[SimpleError] = io.circe.generic.semiauto.deriveDecoder[SimpleError] + implicit lazy val simpleErrorJsonEncoder: io.circe.Encoder[SimpleError] = io.circe.generic.semiauto.deriveEncoder[SimpleError] + implicit lazy val notFoundErrorJsonDecoder: io.circe.Decoder[NotFoundError] = io.circe.generic.semiauto.deriveDecoder[NotFoundError] + implicit lazy val notFoundErrorJsonEncoder: io.circe.Encoder[NotFoundError] = io.circe.generic.semiauto.deriveEncoder[NotFoundError] implicit lazy val subtypeWithoutD1JsonDecoder: io.circe.Decoder[SubtypeWithoutD1] = io.circe.generic.semiauto.deriveDecoder[SubtypeWithoutD1] implicit lazy val subtypeWithoutD1JsonEncoder: io.circe.Encoder[SubtypeWithoutD1] = io.circe.generic.semiauto.deriveEncoder[SubtypeWithoutD1] implicit lazy val subtypeWithD1JsonDecoder: io.circe.Decoder[SubtypeWithD1] = io.circe.generic.semiauto.deriveDecoder[SubtypeWithD1] diff --git a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip/ExpectedSchemas.scala.txt b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip/ExpectedSchemas.scala.txt index 90179526d3..f8a7ec11c4 100644 --- a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip/ExpectedSchemas.scala.txt +++ b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip/ExpectedSchemas.scala.txt @@ -4,8 +4,10 @@ object TapirGeneratedEndpointsSchemas { import sttp.tapir.generated.TapirGeneratedEndpoints._ import sttp.tapir.generic.auto._ implicit lazy val anEnumTapirSchema: sttp.tapir.Schema[AnEnum] = sttp.tapir.Schema.derived + implicit lazy val notFoundErrorTapirSchema: sttp.tapir.Schema[NotFoundError] = sttp.tapir.Schema.derived implicit lazy val objectWithInlineEnumInlineEnumTapirSchema: sttp.tapir.Schema[ObjectWithInlineEnumInlineEnum] = sttp.tapir.Schema.derived implicit lazy val objectWithInlineEnumTapirSchema: sttp.tapir.Schema[ObjectWithInlineEnum] = sttp.tapir.Schema.derived + implicit lazy val simpleErrorTapirSchema: sttp.tapir.Schema[SimpleError] = sttp.tapir.Schema.derived implicit lazy val subtypeWithD1TapirSchema: sttp.tapir.Schema[SubtypeWithD1] = sttp.tapir.Schema.derived implicit lazy val subtypeWithD2TapirSchema: sttp.tapir.Schema[SubtypeWithD2] = sttp.tapir.Schema.derived implicit lazy val subtypeWithoutD1TapirSchema: sttp.tapir.Schema[SubtypeWithoutD1] = sttp.tapir.Schema.derived @@ -38,6 +40,7 @@ object TapirGeneratedEndpointsSchemas { case _ => throw new IllegalStateException("Derived schema for ADTWithDiscriminatorNoMapping should be a coproduct") } } + implicit lazy val errorTapirSchema: sttp.tapir.Schema[Error] = sttp.tapir.Schema.derived implicit lazy val subtypeWithoutD3TapirSchema: sttp.tapir.Schema[SubtypeWithoutD3] = sttp.tapir.Schema.derived implicit lazy val aDTWithoutDiscriminatorTapirSchema: sttp.tapir.Schema[ADTWithoutDiscriminator] = sttp.tapir.Schema.derived } diff --git a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip/build.sbt b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip/build.sbt index dd790d804c..dd0e5c6bae 100644 --- a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip/build.sbt +++ b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip/build.sbt @@ -3,7 +3,8 @@ lazy val root = (project in file(".")) .settings( scalaVersion := "2.13.15", version := "0.1", - openapiStreamingImplementation := "pekko" + openapiStreamingImplementation := "pekko", + openapiGenerateEndpointTypes := true ) libraryDependencies ++= Seq( @@ -12,8 +13,8 @@ libraryDependencies ++= Seq( "com.softwaremill.sttp.tapir" %% "tapir-pekko-http-server" % "1.10.0", "com.softwaremill.sttp.apispec" %% "openapi-circe-yaml" % "0.8.0", "io.circe" %% "circe-generic" % "0.14.9", - "com.beachape" %% "enumeratum" % "1.7.4", - "com.beachape" %% "enumeratum-circe" % "1.7.4", + "com.beachape" %% "enumeratum" % "1.7.5", + "com.beachape" %% "enumeratum-circe" % "1.7.5", "org.scalatest" %% "scalatest" % "3.2.19" % Test, "com.softwaremill.sttp.tapir" %% "tapir-sttp-stub-server" % "1.10.0" % Test ) diff --git a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip/project/build.properties b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip/project/build.properties index 0b699c3052..e88a0d817d 100644 --- a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip/project/build.properties +++ b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.2 +sbt.version=1.10.6 diff --git a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip/swagger.yaml b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip/swagger.yaml index c380f44bd7..20b5b94910 100644 --- a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip/swagger.yaml +++ b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip/swagger.yaml @@ -127,6 +127,24 @@ paths: application/json: schema: $ref: '#/components/schemas/ObjectWithInlineEnum' + '/oneof/error/test': + get: + responses: + "204": + description: "No response" + "404": + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/NotFoundError' + default: + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/SimpleError' + components: schemas: @@ -247,4 +265,26 @@ components: - foo1 - foo2 - foo3 - - foo4 \ No newline at end of file + - foo4 + Error: + title: Error + type: object + oneOf: + - $ref: '#/components/schemas/NotFoundError' + - $ref: '#/components/schemas/SimpleError' + NotFoundError: + title: NotFoundError + required: + - reason + type: object + properties: + reason: + type: string + SimpleError: + title: SimpleError + required: + - message + type: object + properties: + message: + type: string \ No newline at end of file diff --git a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter/build.sbt b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter/build.sbt index cd6b79c9fc..1a50a301df 100644 --- a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter/build.sbt +++ b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter/build.sbt @@ -10,9 +10,9 @@ libraryDependencies ++= Seq( "com.softwaremill.sttp.tapir" %% "tapir-jsoniter-scala" % "1.10.0", "com.softwaremill.sttp.tapir" %% "tapir-openapi-docs" % "1.10.0", "com.softwaremill.sttp.apispec" %% "openapi-circe-yaml" % "0.8.0", - "com.beachape" %% "enumeratum" % "1.7.4", - "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % "2.30.11", - "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % "2.30.11" % "compile-internal", + "com.beachape" %% "enumeratum" % "1.7.5", + "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % "2.32.0", + "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % "2.32.0" % "compile-internal", "org.scalatest" %% "scalatest" % "3.2.19" % Test, "com.softwaremill.sttp.tapir" %% "tapir-sttp-stub-server" % "1.10.0" % Test ) diff --git a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter/project/build.properties b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter/project/build.properties index 0b699c3052..e88a0d817d 100644 --- a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter/project/build.properties +++ b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.2 +sbt.version=1.10.6 diff --git a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter_scala3/build.sbt b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter_scala3/build.sbt index 13f8f435d0..cb914321ab 100644 --- a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter_scala3/build.sbt +++ b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter_scala3/build.sbt @@ -10,9 +10,9 @@ libraryDependencies ++= Seq( "com.softwaremill.sttp.tapir" %% "tapir-jsoniter-scala" % "1.10.0", "com.softwaremill.sttp.tapir" %% "tapir-openapi-docs" % "1.10.0", "com.softwaremill.sttp.apispec" %% "openapi-circe-yaml" % "0.8.0", - "com.beachape" %% "enumeratum" % "1.7.4", - "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % "2.30.11", - "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % "2.30.11" % "compile-internal", + "com.beachape" %% "enumeratum" % "1.7.5", + "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % "2.32.0", + "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % "2.32.0" % "compile-internal", "org.scalatest" %% "scalatest" % "3.2.19" % Test, "com.softwaremill.sttp.tapir" %% "tapir-sttp-stub-server" % "1.10.0" % Test ) diff --git a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter_scala3/project/build.properties b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter_scala3/project/build.properties index 0b699c3052..e88a0d817d 100644 --- a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter_scala3/project/build.properties +++ b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter_scala3/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.2 +sbt.version=1.10.6 diff --git a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_scala3/project/build.properties b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_scala3/project/build.properties index 0b699c3052..e88a0d817d 100644 --- a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_scala3/project/build.properties +++ b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_scala3/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.2 +sbt.version=1.10.6 diff --git a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/option-overrides/project/build.properties b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/option-overrides/project/build.properties index 0b699c3052..e88a0d817d 100644 --- a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/option-overrides/project/build.properties +++ b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/option-overrides/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.2 +sbt.version=1.10.6 diff --git a/perf-tests/src/main/resources/64KB.json b/perf-tests/src/main/resources/64KB.json new file mode 100644 index 0000000000..c2a33dc044 --- /dev/null +++ b/perf-tests/src/main/resources/64KB.json @@ -0,0 +1,1381 @@ +[ + { + "name": "Adeel Solangi", + "language": "Sindhi", + "id": "V59OF92YF627HFY0", + "bio": "Donec lobortis eleifend condimentum. Cras dictum dolor lacinia lectus vehicula rutrum. Maecenas quis nisi nunc. Nam tristique feugiat est vitae mollis. Maecenas quis nisi nunc.", + "version": 6.1 + }, + { + "name": "Afzal Ghaffar", + "language": "Sindhi", + "id": "ENTOCR13RSCLZ6KU", + "bio": "Aliquam sollicitudin ante ligula, eget malesuada nibh efficitur et. Pellentesque massa sem, scelerisque sit amet odio id, cursus tempor urna. Etiam congue dignissim volutpat. Vestibulum pharetra libero et velit gravida euismod.", + "version": 1.88 + }, + { + "name": "Aamir Solangi", + "language": "Sindhi", + "id": "IAKPO3R4761JDRVG", + "bio": "Vestibulum pharetra libero et velit gravida euismod. Quisque mauris ligula, efficitur porttitor sodales ac, lacinia non ex. Fusce eu ultrices elit, vel posuere neque.", + "version": 7.27 + }, + { + "name": "Abla Dilmurat", + "language": "Uyghur", + "id": "5ZVOEPMJUI4MB4EN", + "bio": "Donec lobortis eleifend condimentum. Morbi ac tellus erat.", + "version": 2.53 + }, + { + "name": "Adil Eli", + "language": "Uyghur", + "id": "6VTI8X6LL0MMPJCC", + "bio": "Vivamus id faucibus velit, id posuere leo. Morbi vitae nisi lacinia, laoreet lorem nec, egestas orci. Suspendisse potenti.", + "version": 6.49 + }, + { + "name": "Adile Qadir", + "language": "Uyghur", + "id": "F2KEU5L7EHYSYFTT", + "bio": "Duis commodo orci ut dolor iaculis facilisis. Morbi ultricies consequat ligula posuere eleifend. Aenean finibus in tortor vel aliquet. Fusce eu ultrices elit, vel posuere neque.", + "version": 1.9 + }, + { + "name": "Abdukerim Ibrahim", + "language": "Uyghur", + "id": "LO6DVTZLRK68528I", + "bio": "Vivamus id faucibus velit, id posuere leo. Nunc aliquet sodales nunc a pulvinar. Nunc aliquet sodales nunc a pulvinar. Ut viverra quis eros eu tincidunt.", + "version": 5.9 + }, + { + "name": "Adil Abro", + "language": "Sindhi", + "id": "LJRIULRNJFCNZJAJ", + "bio": "Etiam malesuada blandit erat, nec ultricies leo maximus sed. Fusce congue aliquam elit ut luctus. Etiam malesuada blandit erat, nec ultricies leo maximus sed. Cras dictum dolor lacinia lectus vehicula rutrum. Integer vehicula, arcu sit amet egestas efficitur, orci justo interdum massa, eget ullamcorper risus ligula tristique libero.", + "version": 9.32 + }, + { + "name": "Afonso Vilarchán", + "language": "Galician", + "id": "JMCL0CXNXHPL1GBC", + "bio": "Fusce eu ultrices elit, vel posuere neque. Morbi ac tellus erat. Nunc tincidunt laoreet laoreet.", + "version": 5.21 + }, + { + "name": "Mark Schembri", + "language": "Maltese", + "id": "KU4T500C830697CW", + "bio": "Nam laoreet, nunc non suscipit interdum, justo turpis vestibulum massa, non vulputate ex urna at purus. Morbi ultricies consequat ligula posuere eleifend. Vivamus id faucibus velit, id posuere leo. Sed laoreet posuere sapien, ut feugiat nibh gravida at. Ut maximus, libero nec facilisis fringilla, ex sem sollicitudin leo, non congue tortor ligula in eros.", + "version": 3.17 + }, + { + "name": "Antía Sixirei", + "language": "Galician", + "id": "XOF91ZR7MHV1TXRS", + "bio": "Pellentesque massa sem, scelerisque sit amet odio id, cursus tempor urna. Phasellus massa ligula, hendrerit eget efficitur eget, tincidunt in ligula. Morbi finibus dui sed est fringilla ornare. Duis pellentesque ultrices convallis. Morbi ultricies consequat ligula posuere eleifend.", + "version": 6.44 + }, + { + "name": "Aygul Mutellip", + "language": "Uyghur", + "id": "FTSNV411G5MKLPDT", + "bio": "Duis commodo orci ut dolor iaculis facilisis. Nam semper gravida nunc, sit amet elementum ipsum. Donec pellentesque ultrices mi, non consectetur eros luctus non. Pellentesque massa sem, scelerisque sit amet odio id, cursus tempor urna.", + "version": 9.1 + }, + { + "name": "Awais Shaikh", + "language": "Sindhi", + "id": "OJMWMEEQWMLDU29P", + "bio": "Nunc aliquet sodales nunc a pulvinar. Ut dictum, ligula eget sagittis maximus, tellus mi varius ex, a accumsan justo tellus vitae leo. Donec pellentesque ultrices mi, non consectetur eros luctus non. Nulla finibus massa at viverra facilisis. Nunc tincidunt laoreet laoreet.", + "version": 1.59 + }, + { + "name": "Ambreen Ahmed", + "language": "Sindhi", + "id": "5G646V7E6TJW8X2M", + "bio": "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Etiam consequat enim lorem, at tincidunt velit ultricies et. Ut maximus, libero nec facilisis fringilla, ex sem sollicitudin leo, non congue tortor ligula in eros.", + "version": 2.35 + }, + { + "name": "Celtia Anes", + "language": "Galician", + "id": "Z53AJY7WUYPLAWC9", + "bio": "Nullam ac sodales dolor, eu facilisis dui. Maecenas non arcu nulla. Ut viverra quis eros eu tincidunt. Curabitur quis commodo quam.", + "version": 8.34 + }, + { + "name": "George Mifsud", + "language": "Maltese", + "id": "N1AS6UFULO6WGTLB", + "bio": "Phasellus tincidunt sollicitudin posuere. Ut accumsan, est vel fringilla varius, purus augue blandit nisl, eu rhoncus ligula purus vel dolor. Donec congue sapien vel euismod interdum. Cras dictum dolor lacinia lectus vehicula rutrum. Phasellus massa ligula, hendrerit eget efficitur eget, tincidunt in ligula.", + "version": 7.47 + }, + { + "name": "Aytürk Qasim", + "language": "Uyghur", + "id": "70RODUVRD95CLOJL", + "bio": "Curabitur ultricies id urna nec ultrices. Aliquam scelerisque pretium tellus, sed accumsan est ultrices id. Duis commodo orci ut dolor iaculis facilisis.", + "version": 1.32 + }, + { + "name": "Dialè Meso", + "language": "Sesotho sa Leboa", + "id": "VBLI24FKF7VV6BWE", + "bio": "Maecenas non arcu nulla. Vivamus id faucibus velit, id posuere leo. Nullam sodales convallis mauris, sit amet lobortis magna auctor sit amet.", + "version": 6.29 + }, + { + "name": "Breixo Galáns", + "language": "Galician", + "id": "4VRLON0GPEZYFCVL", + "bio": "Integer vehicula, arcu sit amet egestas efficitur, orci justo interdum massa, eget ullamcorper risus ligula tristique libero. Morbi ac tellus erat. In id elit malesuada, pulvinar mi eu, imperdiet nulla. Vestibulum pharetra libero et velit gravida euismod. Cras dictum dolor lacinia lectus vehicula rutrum.", + "version": 1.62 + }, + { + "name": "Bieito Lorme", + "language": "Galician", + "id": "5DRDI1QLRGLP29RC", + "bio": "Ut viverra quis eros eu tincidunt. Morbi vitae nisi lacinia, laoreet lorem nec, egestas orci. Curabitur quis commodo quam. Morbi ac tellus erat.", + "version": 4.45 + }, + { + "name": "Azrugul Osman", + "language": "Uyghur", + "id": "5RCTVD3C5QGVAKTQ", + "bio": "Maecenas tempus neque ut porttitor malesuada. Donec lobortis eleifend condimentum.", + "version": 3.18 + }, + { + "name": "Brais Verdiñas", + "language": "Galician", + "id": "BT407GHCC0IHXCD3", + "bio": "Quisque maximus sodales mauris ut elementum. Ut viverra quis eros eu tincidunt. Sed eu libero maximus nunc lacinia lobortis et sit amet nisi. In id elit malesuada, pulvinar mi eu, imperdiet nulla. Curabitur quis commodo quam.", + "version": 5.01 + }, + { + "name": "Ekber Sadir", + "language": "Uyghur", + "id": "AGZDAP8D8OVRRLTY", + "bio": "Quisque efficitur vel sapien ut imperdiet. Phasellus massa ligula, hendrerit eget efficitur eget, tincidunt in ligula. In id elit malesuada, pulvinar mi eu, imperdiet nulla. Sed nec suscipit ligula. Integer vehicula, arcu sit amet egestas efficitur, orci justo interdum massa, eget ullamcorper risus ligula tristique libero.", + "version": 2.04 + }, + { + "name": "Doreen Bartolo", + "language": "Maltese", + "id": "59QSX02O2XOZGRLH", + "bio": "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Etiam consequat enim lorem, at tincidunt velit ultricies et. Nam semper gravida nunc, sit amet elementum ipsum. Ut viverra quis eros eu tincidunt. Curabitur sed condimentum felis, ut luctus eros.", + "version": 9.31 + }, + { + "name": "Ali Ayaz", + "language": "Sindhi", + "id": "3WNLUZ5LT2F7MYVU", + "bio": "Cras dictum dolor lacinia lectus vehicula rutrum. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Etiam consequat enim lorem, at tincidunt velit ultricies et. Etiam malesuada blandit erat, nec ultricies leo maximus sed.", + "version": 7.8 + }, + { + "name": "Guzelnur Polat", + "language": "Uyghur", + "id": "I6QQHAEGV4CYDXLP", + "bio": "Nam laoreet, nunc non suscipit interdum, justo turpis vestibulum massa, non vulputate ex urna at purus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Etiam consequat enim lorem, at tincidunt velit ultricies et. Nulla finibus massa at viverra facilisis.", + "version": 8.56 + }, + { + "name": "John Falzon", + "language": "Maltese", + "id": "U3AWXHDTSU0H82SL", + "bio": "Sed nec suscipit ligula. Nullam sodales convallis mauris, sit amet lobortis magna auctor sit amet.", + "version": 9.96 + }, + { + "name": "Erkin Qadir", + "language": "Uyghur", + "id": "GV6TA1AATZYBJ3VR", + "bio": "Phasellus massa ligula, hendrerit eget efficitur eget, tincidunt in ligula. .", + "version": 3.53 + }, + { + "name": "Anita Rajput", + "language": "Sindhi", + "id": "XLLVD0NO2ZFEP4AK", + "bio": "Nam semper gravida nunc, sit amet elementum ipsum. Etiam congue dignissim volutpat.", + "version": 5.16 + }, + { + "name": "Ayesha Khalique", + "language": "Sindhi", + "id": "Q9A5QNGA0OSU8P6Y", + "bio": "Morbi vitae nisi lacinia, laoreet lorem nec, egestas orci. Etiam mauris magna, fermentum vitae aliquet eu, cursus vitae sapien.", + "version": 3.9 + }, + { + "name": "Pheladi Rammala", + "language": "Sesotho sa Leboa", + "id": "EELSIRT2T4Q0M3M4", + "bio": "Quisque efficitur vel sapien ut imperdiet. Morbi ac tellus erat. Aliquam scelerisque pretium tellus, sed accumsan est ultrices id. Ut maximus, libero nec facilisis fringilla, ex sem sollicitudin leo, non congue tortor ligula in eros.", + "version": 1.88 + }, + { + "name": "Antón Caneiro", + "language": "Galician", + "id": "ENTAPNU3MMFUGM1W", + "bio": "Integer vehicula, arcu sit amet egestas efficitur, orci justo interdum massa, eget ullamcorper risus ligula tristique libero. Vestibulum pharetra libero et velit gravida euismod.", + "version": 4.84 + }, + { + "name": "Qahar Abdulla", + "language": "Uyghur", + "id": "OGLODUPEHKEW0K83", + "bio": "Duis commodo orci ut dolor iaculis facilisis. Aliquam sollicitudin ante ligula, eget malesuada nibh efficitur et. Fusce congue aliquam elit ut luctus. Integer vehicula, arcu sit amet egestas efficitur, orci justo interdum massa, eget ullamcorper risus ligula tristique libero. Quisque maximus sodales mauris ut elementum.", + "version": 3.65 + }, + { + "name": "Reyhan Murat", + "language": "Uyghur", + "id": "Y91F4D54794E9ANT", + "bio": "Suspendisse sit amet ullamcorper sem. Curabitur sed condimentum felis, ut luctus eros.", + "version": 2.69 + }, + { + "name": "Tatapi Phogole", + "language": "Sesotho sa Leboa", + "id": "7JA42P5CMCWDVPNR", + "bio": "Duis luctus, lacus eu aliquet convallis, purus elit malesuada ex, vitae rutrum ipsum dui ut magna. Nullam ac sodales dolor, eu facilisis dui. Ut viverra quis eros eu tincidunt.", + "version": 3.78 + }, + { + "name": "Marcos Amboade", + "language": "Galician", + "id": "WPX7H97C7D70CZJR", + "bio": "Nulla finibus massa at viverra facilisis. Pellentesque massa sem, scelerisque sit amet odio id, cursus tempor urna. Curabitur ultricies id urna nec ultrices. Ut maximus, libero nec facilisis fringilla, ex sem sollicitudin leo, non congue tortor ligula in eros. Nunc aliquet sodales nunc a pulvinar.", + "version": 7.37 + }, + { + "name": "Grace Tabone", + "language": "Maltese", + "id": "K4XO8G8DMRNSHF2B", + "bio": "Curabitur sed condimentum felis, ut luctus eros. Duis luctus, lacus eu aliquet convallis, purus elit malesuada ex, vitae rutrum ipsum dui ut magna.", + "version": 5.36 + }, + { + "name": "Shafqat Memon", + "language": "Sindhi", + "id": "D8VFLVRXBXMVBRVI", + "bio": "Aliquam scelerisque pretium tellus, sed accumsan est ultrices id. . Curabitur quis commodo quam. Quisque maximus sodales mauris ut elementum. Quisque mauris ligula, efficitur porttitor sodales ac, lacinia non ex.", + "version": 8.95 + }, + { + "name": "Zeynep Semet", + "language": "Uyghur", + "id": "Z324TZV8S0FGDSAO", + "bio": "Quisque mauris ligula, efficitur porttitor sodales ac, lacinia non ex. Fusce eu ultrices elit, vel posuere neque. Nulla finibus massa at viverra facilisis.", + "version": 1.03 + }, + { + "name": "Meladi Papo", + "language": "Sesotho sa Leboa", + "id": "RJAZQ6BBLRT72CD9", + "bio": "Quisque efficitur vel sapien ut imperdiet. Pellentesque massa sem, scelerisque sit amet odio id, cursus tempor urna. Ut accumsan, est vel fringilla varius, purus augue blandit nisl, eu rhoncus ligula purus vel dolor. Etiam congue dignissim volutpat. Donec congue sapien vel euismod interdum.", + "version": 7.22 + }, + { + "name": "Semet Alim", + "language": "Uyghur", + "id": "HI7L2SR4RCS8C8CS", + "bio": "Duis commodo orci ut dolor iaculis facilisis. Ut viverra quis eros eu tincidunt. Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + "version": 1.01 + }, + { + "name": "Sabela Veloso", + "language": "Galician", + "id": "QA55WXDLC7SRH97X", + "bio": "Duis commodo orci ut dolor iaculis facilisis. Suspendisse potenti. Cras dictum dolor lacinia lectus vehicula rutrum.", + "version": 7.32 + }, + { + "name": "Madule Ledimo", + "language": "Sesotho sa Leboa", + "id": "IHJN2DGJB5O1Y00D", + "bio": "Maecenas non arcu nulla. Aliquam scelerisque pretium tellus, sed accumsan est ultrices id.", + "version": 7.47 + }, + { + "name": "Michelle Caruana", + "language": "Maltese", + "id": "EG1I21R75IV9Q0Q8", + "bio": "Nam tristique feugiat est vitae mollis. Morbi ultricies consequat ligula posuere eleifend. Pellentesque massa sem, scelerisque sit amet odio id, cursus tempor urna.", + "version": 4.95 + }, + { + "name": "Philip Camilleri", + "language": "Maltese", + "id": "FCO0URUHARX5FDFW", + "bio": "Quisque efficitur vel sapien ut imperdiet. Suspendisse sit amet ullamcorper sem. Aliquam scelerisque pretium tellus, sed accumsan est ultrices id. . Aenean finibus in tortor vel aliquet.", + "version": 9.97 + }, + { + "name": "Olalla Romeu", + "language": "Galician", + "id": "WOCMVO6CYPG01ZHY", + "bio": "Maecenas tempus neque ut porttitor malesuada. Sed nec suscipit ligula. Morbi vitae nisi lacinia, laoreet lorem nec, egestas orci. Nullam sodales convallis mauris, sit amet lobortis magna auctor sit amet.", + "version": 1.98 + }, + { + "name": "Gulnur Perhat", + "language": "Uyghur", + "id": "VO3M22TTQMBA2XEM", + "bio": "Nullam ac sodales dolor, eu facilisis dui. Etiam mauris magna, fermentum vitae aliquet eu, cursus vitae sapien. Ut accumsan, est vel fringilla varius, purus augue blandit nisl, eu rhoncus ligula purus vel dolor. Maecenas quis nisi nunc. Duis pellentesque ultrices convallis.", + "version": 5.03 + }, + { + "name": "Hunadi Makgatho", + "language": "Sesotho sa Leboa", + "id": "MRJDOV2MU7PTCDXE", + "bio": "Phasellus tincidunt sollicitudin posuere. Maecenas quis nisi nunc. Duis luctus, lacus eu aliquet convallis, purus elit malesuada ex, vitae rutrum ipsum dui ut magna.", + "version": 8.18 + }, + { + "name": "Charmaine Abela", + "language": "Maltese", + "id": "F6FJP1QDJL944X4Z", + "bio": "Nam rutrum sollicitudin ante tempus consequat. Suspendisse sit amet ullamcorper sem. Morbi ac tellus erat. Sed nec suscipit ligula.", + "version": 6.95 + }, + { + "name": "Tumelò Letamo", + "language": "Sesotho sa Leboa", + "id": "F8BL9NPIKV0OWO1X", + "bio": "Aliquam sollicitudin ante ligula, eget malesuada nibh efficitur et. Etiam congue dignissim volutpat. Sed nec suscipit ligula. Nullam sodales convallis mauris, sit amet lobortis magna auctor sit amet.", + "version": 7.17 + }, + { + "name": "Aneela Mohan", + "language": "Sindhi", + "id": "CRYN52CXKNJU0YXU", + "bio": "Sed nec suscipit ligula. Phasellus massa ligula, hendrerit eget efficitur eget, tincidunt in ligula. Maecenas tempus neque ut porttitor malesuada.", + "version": 4.45 + }, + { + "name": "Koketšo Montjane", + "language": "Sesotho sa Leboa", + "id": "0TTAMXC9TENQCA2O", + "bio": "Curabitur sed condimentum felis, ut luctus eros. Aliquam sollicitudin ante ligula, eget malesuada nibh efficitur et. Etiam mauris magna, fermentum vitae aliquet eu, cursus vitae sapien. Ut maximus, libero nec facilisis fringilla, ex sem sollicitudin leo, non congue tortor ligula in eros.", + "version": 3.61 + }, + { + "name": "Tegra Núnez", + "language": "Galician", + "id": "NC1ZUV6B853BZZCW", + "bio": "Maecenas tempus neque ut porttitor malesuada. Pellentesque massa sem, scelerisque sit amet odio id, cursus tempor urna.", + "version": 6.68 + }, + { + "name": "Dilnur Qeyser", + "language": "Uyghur", + "id": "JVQ8RQ4YRPGLFMR8", + "bio": "Maecenas non arcu nulla. Nulla finibus massa at viverra facilisis. Integer vehicula, arcu sit amet egestas efficitur, orci justo interdum massa, eget ullamcorper risus ligula tristique libero. Ut maximus, libero nec facilisis fringilla, ex sem sollicitudin leo, non congue tortor ligula in eros.", + "version": 7.93 + }, + { + "name": "Tania Agius", + "language": "Maltese", + "id": "WTDGKLDWJLR1BJKR", + "bio": "Etiam congue dignissim volutpat. Pellentesque massa sem, scelerisque sit amet odio id, cursus tempor urna.", + "version": 4.78 + }, + { + "name": "Iago Peirallo", + "language": "Galician", + "id": "D51G7XQTX2SPHR52", + "bio": "Aliquam sollicitudin ante ligula, eget malesuada nibh efficitur et. Donec congue sapien vel euismod interdum. Suspendisse potenti. Quisque maximus sodales mauris ut elementum. Quisque maximus sodales mauris ut elementum.", + "version": 6.3 + }, + { + "name": "Mpho Lamola", + "language": "Sesotho sa Leboa", + "id": "UGL8EOTXYBW1ILLW", + "bio": "In id elit malesuada, pulvinar mi eu, imperdiet nulla. Curabitur ultricies id urna nec ultrices. Maecenas tempus neque ut porttitor malesuada. In sed ultricies lorem. Nullam sodales convallis mauris, sit amet lobortis magna auctor sit amet.", + "version": 2.05 + }, + { + "name": "Josephine Balzan", + "language": "Maltese", + "id": "4OLTG6QD0A2VB432", + "bio": "Maecenas tempus neque ut porttitor malesuada. Sed eu libero maximus nunc lacinia lobortis et sit amet nisi. Maecenas non arcu nulla. Ut accumsan, est vel fringilla varius, purus augue blandit nisl, eu rhoncus ligula purus vel dolor. Curabitur quis commodo quam.", + "version": 7.64 + }, + { + "name": "Thabò Motongwane", + "language": "Sesotho sa Leboa", + "id": "NROE4ZZVGKZGDFNO", + "bio": "Donec pellentesque ultrices mi, non consectetur eros luctus non. Suspendisse potenti. Suspendisse potenti.", + "version": 2.07 + }, + { + "name": "Mmathabò Mojapelo", + "language": "Sesotho sa Leboa", + "id": "VXJDXYPV5L300IFW", + "bio": "Sed laoreet posuere sapien, ut feugiat nibh gravida at. Duis luctus, lacus eu aliquet convallis, purus elit malesuada ex, vitae rutrum ipsum dui ut magna. Nunc tincidunt laoreet laoreet. .", + "version": 9.36 + }, + { + "name": "Kgabo Lerumo", + "language": "Sesotho sa Leboa", + "id": "D63WWKQE2R4TFDIL", + "bio": "Vestibulum pharetra libero et velit gravida euismod. Maecenas tempus neque ut porttitor malesuada. Morbi ultricies consequat ligula posuere eleifend. Quisque efficitur vel sapien ut imperdiet. Nam rutrum sollicitudin ante tempus consequat.", + "version": 6.69 + }, + { + "name": "Lawrence Scicluna", + "language": "Maltese", + "id": "0KDA7XKZNNZWL2SR", + "bio": "Donec pellentesque ultrices mi, non consectetur eros luctus non. In id elit malesuada, pulvinar mi eu, imperdiet nulla. Aliquam sollicitudin ante ligula, eget malesuada nibh efficitur et.", + "version": 6.53 + }, + { + "name": "Iria Xamardo", + "language": "Galician", + "id": "ULUDKBP9PHBGHX2J", + "bio": "Vivamus id faucibus velit, id posuere leo. Sed eu libero maximus nunc lacinia lobortis et sit amet nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam malesuada blandit erat, nec ultricies leo maximus sed. Ut viverra quis eros eu tincidunt.", + "version": 3.42 + }, + { + "name": "Joseph Grech", + "language": "Maltese", + "id": "T4P1164RJBJ8S6XD", + "bio": "Aliquam scelerisque pretium tellus, sed accumsan est ultrices id. Donec lobortis eleifend condimentum.", + "version": 7.68 + }, + { + "name": "Napogadi Selepe", + "language": "Sesotho sa Leboa", + "id": "AJK91MKRFIHAQHHG", + "bio": "Quisque maximus sodales mauris ut elementum. Maecenas quis nisi nunc.", + "version": 4.95 + }, + { + "name": "Lesetja Theko", + "language": "Sesotho sa Leboa", + "id": "AATM20BURO1DHDAE", + "bio": "Phasellus massa ligula, hendrerit eget efficitur eget, tincidunt in ligula. Etiam mauris magna, fermentum vitae aliquet eu, cursus vitae sapien. Nulla finibus massa at viverra facilisis. Morbi finibus dui sed est fringilla ornare.", + "version": 6.81 + }, + { + "name": "Martiño Arxíz", + "language": "Galician", + "id": "CQ56N9MH3WK7H5YQ", + "bio": "Proin tempus eu risus nec mattis. Morbi vitae nisi lacinia, laoreet lorem nec, egestas orci. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Etiam consequat enim lorem, at tincidunt velit ultricies et. Nam rutrum sollicitudin ante tempus consequat. .", + "version": 7.13 + }, + { + "name": "Malehumò Ledwaba", + "language": "Sesotho sa Leboa", + "id": "E4F3HGRTKQKCT1SE", + "bio": "Ut accumsan, est vel fringilla varius, purus augue blandit nisl, eu rhoncus ligula purus vel dolor. Curabitur quis commodo quam. Quisque maximus sodales mauris ut elementum. Curabitur sed condimentum felis, ut luctus eros. Curabitur ultricies id urna nec ultrices.", + "version": 6.52 + }, + { + "name": "Musa Yasin", + "language": "Uyghur", + "id": "1AF8GIQZ1LF8QW0U", + "bio": "Phasellus tincidunt sollicitudin posuere. Phasellus massa ligula, hendrerit eget efficitur eget, tincidunt in ligula. Ut maximus, libero nec facilisis fringilla, ex sem sollicitudin leo, non congue tortor ligula in eros. Ut accumsan, est vel fringilla varius, purus augue blandit nisl, eu rhoncus ligula purus vel dolor.", + "version": 1.54 + }, + { + "name": "Lajwanti Kumari", + "language": "Sindhi", + "id": "INRW3R54RAY7J9IS", + "bio": "In sed ultricies lorem. Sed eu libero maximus nunc lacinia lobortis et sit amet nisi. Quisque mauris ligula, efficitur porttitor sodales ac, lacinia non ex. Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + "version": 9.34 + }, + { + "name": "Maria Sammut", + "language": "Maltese", + "id": "BJRF0BWIHJ0Q12A1", + "bio": "Maecenas tempus neque ut porttitor malesuada. Curabitur ultricies id urna nec ultrices.", + "version": 6.83 + }, + { + "name": "Rita Busuttil", + "language": "Maltese", + "id": "1QLMU6QZ7EYUNNZV", + "bio": "Phasellus tincidunt sollicitudin posuere. Quisque efficitur vel sapien ut imperdiet. Vestibulum pharetra libero et velit gravida euismod. Maecenas tempus neque ut porttitor malesuada.", + "version": 2.09 + }, + { + "name": "Roi Fraguela", + "language": "Galician", + "id": "UAT0M2O42E9M4SFT", + "bio": "Donec congue sapien vel euismod interdum. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce congue aliquam elit ut luctus. Morbi ac tellus erat. Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + "version": 1.08 + }, + { + "name": "Matome Molamo", + "language": "Sesotho sa Leboa", + "id": "7HI0UZZLRB9N5CBI", + "bio": "Vestibulum pharetra libero et velit gravida euismod. Fusce eu ultrices elit, vel posuere neque. Duis pellentesque ultrices convallis.", + "version": 9.55 + }, + { + "name": "Mapula Selokela", + "language": "Sesotho sa Leboa", + "id": "6ZQTOKQI6K82EE9Q", + "bio": "Duis pellentesque ultrices convallis. Nam laoreet, nunc non suscipit interdum, justo turpis vestibulum massa, non vulputate ex urna at purus. Ut viverra quis eros eu tincidunt. Proin tempus eu risus nec mattis.", + "version": 5.27 + }, + { + "name": "Noa Ervello", + "language": "Galician", + "id": "W9FR842CI16V8NU3", + "bio": "Aliquam sollicitudin ante ligula, eget malesuada nibh efficitur et. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Etiam consequat enim lorem, at tincidunt velit ultricies et. Suspendisse sit amet ullamcorper sem. Quisque mauris ligula, efficitur porttitor sodales ac, lacinia non ex.", + "version": 9.33 + }, + { + "name": "Naseem Kakepoto", + "language": "Sindhi", + "id": "6C7HZV4WPV9C9KS6", + "bio": "Morbi ultricies consequat ligula posuere eleifend. Fusce congue aliquam elit ut luctus. . Phasellus massa ligula, hendrerit eget efficitur eget, tincidunt in ligula.", + "version": 1.4 + }, + { + "name": "sayama Amir", + "language": "Sindhi", + "id": "7K4IJT1X7G0EK9WC", + "bio": "Morbi ac tellus erat. Aliquam scelerisque pretium tellus, sed accumsan est ultrices id. Maecenas quis nisi nunc. Etiam congue dignissim volutpat. Sed nec suscipit ligula.", + "version": 9.48 + }, + { + "name": "Mariña Quintá", + "language": "Galician", + "id": "7GXC4OQYXX5JJY9F", + "bio": "Phasellus tincidunt sollicitudin posuere. Morbi ac tellus erat. Nullam ac sodales dolor, eu facilisis dui.", + "version": 8.81 + }, + { + "name": "Memet Tursun", + "language": "Uyghur", + "id": "KSFMV2JK2D553083", + "bio": "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Etiam consequat enim lorem, at tincidunt velit ultricies et. Morbi finibus dui sed est fringilla ornare. Suspendisse sit amet ullamcorper sem.", + "version": 7.56 + }, + { + "name": "Carmen Vella", + "language": "Maltese", + "id": "WUALBIMS4E8JS4L2", + "bio": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc aliquet sodales nunc a pulvinar. Morbi vitae nisi lacinia, laoreet lorem nec, egestas orci. Vestibulum pharetra libero et velit gravida euismod.", + "version": 4.55 + }, + { + "name": "Sobia Khanam", + "language": "Sindhi", + "id": "YG1ERFWBJ7TIW35D", + "bio": "Phasellus tincidunt sollicitudin posuere. Aliquam scelerisque pretium tellus, sed accumsan est ultrices id. Morbi ultricies consequat ligula posuere eleifend. Curabitur sed condimentum felis, ut luctus eros.", + "version": 4.59 + }, + { + "name": "Raheela Ali", + "language": "Sindhi", + "id": "7JGX9SMLD5DE2IMG", + "bio": "Morbi finibus dui sed est fringilla ornare. Maecenas quis nisi nunc. Maecenas tempus neque ut porttitor malesuada. Curabitur ultricies id urna nec ultrices.", + "version": 4.75 + }, + { + "name": "Rashid Rajput", + "language": "Sindhi", + "id": "UNBGUGDUATATCLS4", + "bio": "Donec congue sapien vel euismod interdum. Maecenas quis nisi nunc.", + "version": 8.51 + }, + { + "name": "Uxía Feal", + "language": "Galician", + "id": "35ZPXUNH1M6W3ZJP", + "bio": "Vestibulum pharetra libero et velit gravida euismod. Vivamus id faucibus velit, id posuere leo.", + "version": 1.31 + }, + { + "name": "Andrew Fenech", + "language": "Maltese", + "id": "VEYKDKL8L0R0C7GQ", + "bio": "In sed ultricies lorem. Etiam mauris magna, fermentum vitae aliquet eu, cursus vitae sapien. Sed laoreet posuere sapien, ut feugiat nibh gravida at.", + "version": 2.5 + }, + { + "name": "Nicholas Micallef", + "language": "Maltese", + "id": "ZYCAI905154LSICR", + "bio": "Nam tristique feugiat est vitae mollis. Curabitur ultricies id urna nec ultrices. Morbi finibus dui sed est fringilla ornare.", + "version": 6.47 + }, + { + "name": "Paul Borg", + "language": "Maltese", + "id": "8AD5MMJ0TD0NJ6H2", + "bio": "Phasellus massa ligula, hendrerit eget efficitur eget, tincidunt in ligula. Etiam mauris magna, fermentum vitae aliquet eu, cursus vitae sapien.", + "version": 3.77 + }, + { + "name": "Sara Saleem", + "language": "Sindhi", + "id": "5LPKMTZI7OPSJRBA", + "bio": "Maecenas tempus neque ut porttitor malesuada. Etiam congue dignissim volutpat. Proin tempus eu risus nec mattis. Morbi vitae nisi lacinia, laoreet lorem nec, egestas orci. Duis commodo orci ut dolor iaculis facilisis.", + "version": 5.31 + }, + { + "name": "Xurxo Golán", + "language": "Galician", + "id": "526ZUSGXEETODHJK", + "bio": "Ut viverra quis eros eu tincidunt. Morbi finibus dui sed est fringilla ornare. Sed laoreet posuere sapien, ut feugiat nibh gravida at. Duis commodo orci ut dolor iaculis facilisis. In sed ultricies lorem.", + "version": 1.75 + }, + { + "name": "Peter Zammit", + "language": "Maltese", + "id": "NNRT5QWNWO2WLS5V", + "bio": "Duis commodo orci ut dolor iaculis facilisis. Maecenas quis nisi nunc.", + "version": 8.23 + }, + { + "name": "Maname Mohlare", + "language": "Sesotho sa Leboa", + "id": "KZJZ9SD0DIWTIBUC", + "bio": "Quisque mauris ligula, efficitur porttitor sodales ac, lacinia non ex. Vestibulum pharetra libero et velit gravida euismod. Ut accumsan, est vel fringilla varius, purus augue blandit nisl, eu rhoncus ligula purus vel dolor. Sed eu libero maximus nunc lacinia lobortis et sit amet nisi.", + "version": 8.95 + }, + { + "name": "Tshepè Mobu", + "language": "Sesotho sa Leboa", + "id": "8CH586LQR7ZCP73P", + "bio": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus massa at viverra facilisis.", + "version": 7.82 + }, + { + "name": "Monica Lohana", + "language": "Sindhi", + "id": "KP1C2WN3DN1R3Y52", + "bio": "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Etiam consequat enim lorem, at tincidunt velit ultricies et. Aenean finibus in tortor vel aliquet. Nam laoreet, nunc non suscipit interdum, justo turpis vestibulum massa, non vulputate ex urna at purus. Morbi vitae nisi lacinia, laoreet lorem nec, egestas orci.", + "version": 7.95 + }, + { + "name": "Patigul Rahman", + "language": "Uyghur", + "id": "NXMNLB0SOYET1VMN", + "bio": "In sed ultricies lorem. Proin tempus eu risus nec mattis. Nam rutrum sollicitudin ante tempus consequat. Aliquam scelerisque pretium tellus, sed accumsan est ultrices id.", + "version": 2.98 + }, + { + "name": "Joanne Scerri", + "language": "Maltese", + "id": "H8FJ2WKLGGF3K26U", + "bio": "Fusce eu ultrices elit, vel posuere neque. Nulla finibus massa at viverra facilisis. Duis commodo orci ut dolor iaculis facilisis. Nam laoreet, nunc non suscipit interdum, justo turpis vestibulum massa, non vulputate ex urna at purus. Phasellus massa ligula, hendrerit eget efficitur eget, tincidunt in ligula.", + "version": 8.4 + }, + { + "name": "Ratanang Maphutha", + "language": "Sesotho sa Leboa", + "id": "EZXJTQQ2JWPB5DI3", + "bio": "Vivamus id faucibus velit, id posuere leo. Phasellus tincidunt sollicitudin posuere. Duis pellentesque ultrices convallis.", + "version": 9.17 + }, + { + "name": "Kamil Mehmud", + "language": "Uyghur", + "id": "M24A9OMYPSX7FD16", + "bio": "Donec congue sapien vel euismod interdum. Suspendisse potenti. In id elit malesuada, pulvinar mi eu, imperdiet nulla. Nunc aliquet sodales nunc a pulvinar. Ut viverra quis eros eu tincidunt.", + "version": 4.66 + }, + { + "name": "Thobile Mbele", + "language": "isiZulu", + "id": "631M00M8YFFBC5NC", + "bio": "Nunc aliquet sodales nunc a pulvinar. Proin tempus eu risus nec mattis. Proin tempus eu risus nec mattis. Aliquam scelerisque pretium tellus, sed accumsan est ultrices id. Nam laoreet, nunc non suscipit interdum, justo turpis vestibulum massa, non vulputate ex urna at purus.", + "version": 8.96 + }, + { + "name": "Kristján Kristjánsson", + "language": "Icelandic", + "id": "0WT0ZW50DNSTCHKW", + "bio": "Quisque maximus sodales mauris ut elementum. Pellentesque massa sem, scelerisque sit amet odio id, cursus tempor urna. Donec congue sapien vel euismod interdum. Phasellus massa ligula, hendrerit eget efficitur eget, tincidunt in ligula. Donec lobortis eleifend condimentum.", + "version": 8.82 + }, + { + "name": "Stefán Stefánsson", + "language": "Icelandic", + "id": "1UOL8UK8BWAOSYTC", + "bio": "Suspendisse potenti. Duis luctus, lacus eu aliquet convallis, purus elit malesuada ex, vitae rutrum ipsum dui ut magna. Morbi ultricies consequat ligula posuere eleifend.", + "version": 7.87 + }, + { + "name": "Preeti Rajdan", + "language": "Hindi", + "id": "3UN0X88Y4WYH3X8X", + "bio": "In sed ultricies lorem. Vivamus id faucibus velit, id posuere leo. Duis commodo orci ut dolor iaculis facilisis. Nam rutrum sollicitudin ante tempus consequat.", + "version": 9.17 + }, + { + "name": "Sanjay Trivedi", + "language": "Hindi", + "id": "CPHR246457BD01KY", + "bio": "Quisque maximus sodales mauris ut elementum. Morbi ac tellus erat. Maecenas tempus neque ut porttitor malesuada. Cras dictum dolor lacinia lectus vehicula rutrum.", + "version": 8.3 + }, + { + "name": "Smiriti Sisodiya", + "language": "Hindi", + "id": "X3KWIL5KEHTMCKOM", + "bio": "Integer vehicula, arcu sit amet egestas efficitur, orci justo interdum massa, eget ullamcorper risus ligula tristique libero. Morbi finibus dui sed est fringilla ornare.", + "version": 3.27 + }, + { + "name": "Sandeep Benarjee", + "language": "Hindi", + "id": "9TS6CIE3UAIFG2IB", + "bio": "Aliquam sollicitudin ante ligula, eget malesuada nibh efficitur et. Sed nec suscipit ligula. Quisque efficitur vel sapien ut imperdiet. Suspendisse sit amet ullamcorper sem.", + "version": 3.86 + }, + { + "name": "Damir Benic", + "language": "Bosnian", + "id": "QUNL9VBRHUGNOFMJ", + "bio": ". Ut dictum, ligula eget sagittis maximus, tellus mi varius ex, a accumsan justo tellus vitae leo.", + "version": 9.56 + }, + { + "name": "Sigrún Kristjánsdóttir", + "language": "Icelandic", + "id": "BT1Q0NUPKHDVCFLE", + "bio": "Ut dictum, ligula eget sagittis maximus, tellus mi varius ex, a accumsan justo tellus vitae leo. Nulla finibus massa at viverra facilisis.", + "version": 6.78 + }, + { + "name": "Basetsana Thage", + "language": "Setswana", + "id": "R9P3P2IAN7NY2X2Y", + "bio": "Pellentesque massa sem, scelerisque sit amet odio id, cursus tempor urna. Nulla finibus massa at viverra facilisis. Phasellus massa ligula, hendrerit eget efficitur eget, tincidunt in ligula.", + "version": 3.97 + }, + { + "name": "Rajesh Santoshi", + "language": "Hindi", + "id": "OXQTFZHZW8SVE3SY", + "bio": "Donec lobortis eleifend condimentum. Nam rutrum sollicitudin ante tempus consequat. Nullam sodales convallis mauris, sit amet lobortis magna auctor sit amet.", + "version": 8.35 + }, + { + "name": "Margrét Magnúsdóttir", + "language": "Icelandic", + "id": "1P6VZEDGK2XUU97L", + "bio": "Sed eu libero maximus nunc lacinia lobortis et sit amet nisi. Duis pellentesque ultrices convallis. Donec lobortis eleifend condimentum.", + "version": 3.76 + }, + { + "name": "Makhosi Ngiba", + "language": "isiZulu", + "id": "CTM3Y3TZOLC7TPDU", + "bio": "Ut dictum, ligula eget sagittis maximus, tellus mi varius ex, a accumsan justo tellus vitae leo. Suspendisse sit amet ullamcorper sem. Donec lobortis eleifend condimentum. Aenean finibus in tortor vel aliquet. Proin tempus eu risus nec mattis.", + "version": 1.18 + }, + { + "name": "Lorato Bogosi", + "language": "Setswana", + "id": "EEZ0KS5E0RXACAIA", + "bio": "Morbi ultricies consequat ligula posuere eleifend. Nam rutrum sollicitudin ante tempus consequat. Ut dictum, ligula eget sagittis maximus, tellus mi varius ex, a accumsan justo tellus vitae leo. Curabitur ultricies id urna nec ultrices.", + "version": 5.48 + }, + { + "name": "Modisaotsile Bolokwe", + "language": "Setswana", + "id": "DN068KNEOAQ8LM19", + "bio": "Nullam ac sodales dolor, eu facilisis dui. Duis commodo orci ut dolor iaculis facilisis. In id elit malesuada, pulvinar mi eu, imperdiet nulla. Donec congue sapien vel euismod interdum. Sed nec suscipit ligula.", + "version": 4.23 + }, + { + "name": "Mxolisi Mhlongo", + "language": "isiZulu", + "id": "Q2HFB19RPLHIZXKH", + "bio": "Aliquam scelerisque pretium tellus, sed accumsan est ultrices id. Maecenas tempus neque ut porttitor malesuada. . Duis commodo orci ut dolor iaculis facilisis.", + "version": 7.49 + }, + { + "name": "Moni Sisodiya", + "language": "Hindi", + "id": "3CR7CN74GCKXWUQF", + "bio": "Vestibulum pharetra libero et velit gravida euismod. Donec congue sapien vel euismod interdum. Fusce congue aliquam elit ut luctus. Ut viverra quis eros eu tincidunt. Phasellus tincidunt sollicitudin posuere.", + "version": 4.58 + }, + { + "name": "Anna Jónsdóttir", + "language": "Icelandic", + "id": "CKJW1XVW90VWO4Y1", + "bio": "Etiam mauris magna, fermentum vitae aliquet eu, cursus vitae sapien. Donec lobortis eleifend condimentum. Etiam mauris magna, fermentum vitae aliquet eu, cursus vitae sapien.", + "version": 5.78 + }, + { + "name": "Darko Basic", + "language": "Bosnian", + "id": "FWT1CZQOIVRJTXRD", + "bio": "Donec congue sapien vel euismod interdum. Fusce eu ultrices elit, vel posuere neque. Duis luctus, lacus eu aliquet convallis, purus elit malesuada ex, vitae rutrum ipsum dui ut magna.", + "version": 2.27 + }, + { + "name": "Kedibonye Magogwe", + "language": "Setswana", + "id": "PCT0HLRPZLDSSDU1", + "bio": "Aliquam sollicitudin ante ligula, eget malesuada nibh efficitur et. Aliquam sollicitudin ante ligula, eget malesuada nibh efficitur et. Quisque maximus sodales mauris ut elementum.", + "version": 5.57 + }, + { + "name": "Nobuhle Xaba", + "language": "isiZulu", + "id": "5K1K8V1OUUFKQ2UV", + "bio": "Maecenas non arcu nulla. Morbi ac tellus erat.", + "version": 1.18 + }, + { + "name": "Monty Dubey", + "language": "Hindi", + "id": "B7SF955NFGAEBRXU", + "bio": "Maecenas quis nisi nunc. Maecenas tempus neque ut porttitor malesuada. Morbi ultricies consequat ligula posuere eleifend. Ut accumsan, est vel fringilla varius, purus augue blandit nisl, eu rhoncus ligula purus vel dolor.", + "version": 6.69 + }, + { + "name": "Richa Choukse", + "language": "Hindi", + "id": "BADWLBP8CNJNBEC8", + "bio": "Nunc tincidunt laoreet laoreet. Ut dictum, ligula eget sagittis maximus, tellus mi varius ex, a accumsan justo tellus vitae leo. Curabitur quis commodo quam. Morbi vitae nisi lacinia, laoreet lorem nec, egestas orci.", + "version": 7.8 + }, + { + "name": "Dzenan Imamovic", + "language": "Bosnian", + "id": "FVAHD0OY99X9DIRW", + "bio": "Nam tristique feugiat est vitae mollis. Quisque mauris ligula, efficitur porttitor sodales ac, lacinia non ex. Nullam ac sodales dolor, eu facilisis dui. Morbi finibus dui sed est fringilla ornare. Quisque efficitur vel sapien ut imperdiet.", + "version": 1.64 + }, + { + "name": "Amol Bhatnagar", + "language": "Hindi", + "id": "3HPSETKL9VOW2WTL", + "bio": "Vestibulum pharetra libero et velit gravida euismod. Nam semper gravida nunc, sit amet elementum ipsum.", + "version": 3.28 + }, + { + "name": "Ingibjörg Ólafsdóttir", + "language": "Icelandic", + "id": "9BXLMMM1PQOZRHCR", + "bio": "Maecenas non arcu nulla. Sed nec suscipit ligula. Fusce congue aliquam elit ut luctus.", + "version": 9.59 + }, + { + "name": "Shweta Chourasia", + "language": "Hindi", + "id": "9GAO62FXPQMUTTLJ", + "bio": "Duis luctus, lacus eu aliquet convallis, purus elit malesuada ex, vitae rutrum ipsum dui ut magna. Integer vehicula, arcu sit amet egestas efficitur, orci justo interdum massa, eget ullamcorper risus ligula tristique libero. Quisque maximus sodales mauris ut elementum. Sed eu libero maximus nunc lacinia lobortis et sit amet nisi.", + "version": 5.84 + }, + { + "name": "Ayanda Ndimande", + "language": "isiZulu", + "id": "VPK9MQRKX2L847HQ", + "bio": "Duis commodo orci ut dolor iaculis facilisis. Nam laoreet, nunc non suscipit interdum, justo turpis vestibulum massa, non vulputate ex urna at purus.", + "version": 2.89 + }, + { + "name": "Sigurjón Guðmundsson", + "language": "Icelandic", + "id": "IAYT285H2U8JU94F", + "bio": "Sed eu libero maximus nunc lacinia lobortis et sit amet nisi. Ut viverra quis eros eu tincidunt. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam sollicitudin ante ligula, eget malesuada nibh efficitur et. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Etiam consequat enim lorem, at tincidunt velit ultricies et.", + "version": 4.85 + }, + { + "name": "Jóhannes Jóhannsson", + "language": "Icelandic", + "id": "J2RAROEJGKMR72I8", + "bio": "Duis pellentesque ultrices convallis. Nulla finibus massa at viverra facilisis. Ut dictum, ligula eget sagittis maximus, tellus mi varius ex, a accumsan justo tellus vitae leo.", + "version": 4.83 + }, + { + "name": "Neo Dikgaka", + "language": "Setswana", + "id": "OQRF6Y37N20JILOC", + "bio": "Nam tristique feugiat est vitae mollis. Sed nec suscipit ligula. Nam laoreet, nunc non suscipit interdum, justo turpis vestibulum massa, non vulputate ex urna at purus. Duis pellentesque ultrices convallis. Maecenas quis nisi nunc.", + "version": 1.07 + }, + { + "name": "Sanja Jankovic", + "language": "Bosnian", + "id": "HD94EKIPA6WAL05C", + "bio": "Phasellus tincidunt sollicitudin posuere. Nam laoreet, nunc non suscipit interdum, justo turpis vestibulum massa, non vulputate ex urna at purus. In id elit malesuada, pulvinar mi eu, imperdiet nulla. Donec congue sapien vel euismod interdum. Nullam ac sodales dolor, eu facilisis dui.", + "version": 1.06 + }, + { + "name": "Mogorosi Bakwena", + "language": "Setswana", + "id": "FTZM8YDJJUH1OEM7", + "bio": "Vestibulum pharetra libero et velit gravida euismod. Suspendisse sit amet ullamcorper sem.", + "version": 6.03 + }, + { + "name": "Ronak Gupta", + "language": "Hindi", + "id": "ZYPDGK8UDYJPTRKN", + "bio": "Sed laoreet posuere sapien, ut feugiat nibh gravida at. Quisque mauris ligula, efficitur porttitor sodales ac, lacinia non ex. In sed ultricies lorem. Pellentesque massa sem, scelerisque sit amet odio id, cursus tempor urna.", + "version": 7.18 + }, + { + "name": "Ditiro Kgosi", + "language": "Setswana", + "id": "67C5ET66U59WYJ6K", + "bio": "Fusce congue aliquam elit ut luctus. Ut dictum, ligula eget sagittis maximus, tellus mi varius ex, a accumsan justo tellus vitae leo. Cras dictum dolor lacinia lectus vehicula rutrum. Etiam congue dignissim volutpat.", + "version": 4.56 + }, + { + "name": "Jelena Maric", + "language": "Bosnian", + "id": "JTW9DH3B9QGB39JY", + "bio": "Vestibulum pharetra libero et velit gravida euismod. Etiam malesuada blandit erat, nec ultricies leo maximus sed.", + "version": 3.39 + }, + { + "name": "Esha Sastry", + "language": "Hindi", + "id": "4OJULHY03Z6XTRMW", + "bio": "Morbi vitae nisi lacinia, laoreet lorem nec, egestas orci. Nullam ac sodales dolor, eu facilisis dui.", + "version": 5.1 + }, + { + "name": "Chetana Hegde", + "language": "Hindi", + "id": "J9GS1RODDZL325LK", + "bio": "Aliquam sollicitudin ante ligula, eget malesuada nibh efficitur et. Nulla finibus massa at viverra facilisis. Nam tristique feugiat est vitae mollis. Phasellus tincidunt sollicitudin posuere.", + "version": 9.99 + }, + { + "name": "Rahul Shukla", + "language": "Hindi", + "id": "2ANVMAVG6YX2VT6N", + "bio": "Phasellus massa ligula, hendrerit eget efficitur eget, tincidunt in ligula. Phasellus massa ligula, hendrerit eget efficitur eget, tincidunt in ligula.", + "version": 1.72 + }, + { + "name": "Samra Delic", + "language": "Bosnian", + "id": "BXJWNTJ2TDID61PJ", + "bio": "Donec pellentesque ultrices mi, non consectetur eros luctus non. Sed nec suscipit ligula.", + "version": 2.5 + }, + { + "name": "Mohan Pandey", + "language": "Hindi", + "id": "XAHKVLM3I1WSPNIW", + "bio": "Maecenas quis nisi nunc. Ut dictum, ligula eget sagittis maximus, tellus mi varius ex, a accumsan justo tellus vitae leo. Ut maximus, libero nec facilisis fringilla, ex sem sollicitudin leo, non congue tortor ligula in eros. Morbi ac tellus erat.", + "version": 8.1 + }, + { + "name": "Haris Osmanovic", + "language": "Bosnian", + "id": "ZDXF5KESMW9XF2TJ", + "bio": "Nam rutrum sollicitudin ante tempus consequat. Etiam mauris magna, fermentum vitae aliquet eu, cursus vitae sapien.", + "version": 9.41 + }, + { + "name": "Kenosi Kwenaemang", + "language": "Setswana", + "id": "DX2IYTQ9IMY75W08", + "bio": "Sed laoreet posuere sapien, ut feugiat nibh gravida at. Donec lobortis eleifend condimentum.", + "version": 9.01 + }, + { + "name": "Nontobeko Nzimande", + "language": "isiZulu", + "id": "Y9C4HQHTOP74DFZT", + "bio": "Nullam sodales convallis mauris, sit amet lobortis magna auctor sit amet. Morbi vitae nisi lacinia, laoreet lorem nec, egestas orci. Integer vehicula, arcu sit amet egestas efficitur, orci justo interdum massa, eget ullamcorper risus ligula tristique libero. Nam laoreet, nunc non suscipit interdum, justo turpis vestibulum massa, non vulputate ex urna at purus.", + "version": 4.77 + }, + { + "name": "Sanjay Puranik", + "language": "Hindi", + "id": "WF2WP6S0HX8GR8GZ", + "bio": "Ut viverra quis eros eu tincidunt. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Etiam consequat enim lorem, at tincidunt velit ultricies et. Nam semper gravida nunc, sit amet elementum ipsum.", + "version": 3.37 + }, + { + "name": "Sethunya Mpšwe", + "language": "Setswana", + "id": "85MVUXVQ5H5HPA4F", + "bio": "Quisque maximus sodales mauris ut elementum. Duis commodo orci ut dolor iaculis facilisis. Sed eu libero maximus nunc lacinia lobortis et sit amet nisi.", + "version": 1.75 + }, + { + "name": "Dileep Chaturvedi", + "language": "Hindi", + "id": "O95BY1KDMCEYQRFH", + "bio": "Phasellus tincidunt sollicitudin posuere. In id elit malesuada, pulvinar mi eu, imperdiet nulla. Vivamus id faucibus velit, id posuere leo. Nullam ac sodales dolor, eu facilisis dui. Ut dictum, ligula eget sagittis maximus, tellus mi varius ex, a accumsan justo tellus vitae leo.", + "version": 4.94 + }, + { + "name": "Adnan Spahic", + "language": "Bosnian", + "id": "97IIDMHAJMBPI4ON", + "bio": "Duis commodo orci ut dolor iaculis facilisis. Vivamus id faucibus velit, id posuere leo.", + "version": 9.1 + }, + { + "name": "Madhur Jain", + "language": "Hindi", + "id": "FM300CZ0VU9LTNTE", + "bio": "Fusce eu ultrices elit, vel posuere neque. Donec congue sapien vel euismod interdum. Vivamus id faucibus velit, id posuere leo. Integer vehicula, arcu sit amet egestas efficitur, orci justo interdum massa, eget ullamcorper risus ligula tristique libero. Aliquam sollicitudin ante ligula, eget malesuada nibh efficitur et.", + "version": 4.99 + }, + { + "name": "Nayan Mittal", + "language": "Hindi", + "id": "S879KFFIHDNK8GSE", + "bio": "Suspendisse sit amet ullamcorper sem. In id elit malesuada, pulvinar mi eu, imperdiet nulla. Duis commodo orci ut dolor iaculis facilisis.", + "version": 3.99 + }, + { + "name": "Kabelo Morwe", + "language": "Setswana", + "id": "JJDPB2983QRVATD3", + "bio": "Nullam ac sodales dolor, eu facilisis dui. Phasellus massa ligula, hendrerit eget efficitur eget, tincidunt in ligula. . Integer vehicula, arcu sit amet egestas efficitur, orci justo interdum massa, eget ullamcorper risus ligula tristique libero. Curabitur ultricies id urna nec ultrices.", + "version": 8.86 + }, + { + "name": "Einar Einarsson", + "language": "Icelandic", + "id": "ZWMFEUEBNYTW2WPB", + "bio": "Etiam mauris magna, fermentum vitae aliquet eu, cursus vitae sapien. Duis pellentesque ultrices convallis. Nullam sodales convallis mauris, sit amet lobortis magna auctor sit amet. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Etiam consequat enim lorem, at tincidunt velit ultricies et. Donec congue sapien vel euismod interdum.", + "version": 9.05 + }, + { + "name": "Luka Lovren", + "language": "Bosnian", + "id": "9S4SGEQWBKMRISYZ", + "bio": "Maecenas tempus neque ut porttitor malesuada. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur quis commodo quam. Nam rutrum sollicitudin ante tempus consequat.", + "version": 5.22 + }, + { + "name": "Sigríður Einarsdóttir", + "language": "Icelandic", + "id": "4IJVD6OE3C7IX3ZG", + "bio": "Aenean finibus in tortor vel aliquet. Nam tristique feugiat est vitae mollis.", + "version": 6.63 + }, + { + "name": "Sonu Jain", + "language": "Hindi", + "id": "0OIB5SU9JB2PBJDV", + "bio": "Phasellus massa ligula, hendrerit eget efficitur eget, tincidunt in ligula. Curabitur ultricies id urna nec ultrices.", + "version": 9.66 + }, + { + "name": "Boitumelo Ngwako", + "language": "Setswana", + "id": "INZITSS95L9V52JE", + "bio": "Ut maximus, libero nec facilisis fringilla, ex sem sollicitudin leo, non congue tortor ligula in eros. Aliquam sollicitudin ante ligula, eget malesuada nibh efficitur et. Nam tristique feugiat est vitae mollis. Quisque mauris ligula, efficitur porttitor sodales ac, lacinia non ex. In sed ultricies lorem.", + "version": 9.07 + }, + { + "name": "Shilpa Bhatia", + "language": "Hindi", + "id": "SU0W3T6TF8G3JY5M", + "bio": "Morbi ultricies consequat ligula posuere eleifend. Donec pellentesque ultrices mi, non consectetur eros luctus non. Quisque efficitur vel sapien ut imperdiet. Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + "version": 4.43 + }, + { + "name": "Modise Tau", + "language": "Setswana", + "id": "U6SF3N4JXJEQSC1P", + "bio": "Vivamus id faucibus velit, id posuere leo. Phasellus massa ligula, hendrerit eget efficitur eget, tincidunt in ligula. Fusce eu ultrices elit, vel posuere neque. Nunc tincidunt laoreet laoreet.", + "version": 6.23 + }, + { + "name": "Reena Shrivastav", + "language": "Hindi", + "id": "Y57EEOVURYX1OA1P", + "bio": "Donec lobortis eleifend condimentum. Curabitur ultricies id urna nec ultrices. Maecenas non arcu nulla.", + "version": 3.07 + }, + { + "name": "Thabani Ngubani", + "language": "isiZulu", + "id": "LR7FI8WEE3SLTW02", + "bio": "Cras dictum dolor lacinia lectus vehicula rutrum. Nulla finibus massa at viverra facilisis.", + "version": 5.99 + }, + { + "name": "Gunnar Gunnarsson", + "language": "Icelandic", + "id": "UVI6EKJNMC3VE3WU", + "bio": "In sed ultricies lorem. Donec congue sapien vel euismod interdum. Duis commodo orci ut dolor iaculis facilisis. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Etiam consequat enim lorem, at tincidunt velit ultricies et.", + "version": 8.7 + }, + { + "name": "Lejla Selimagic", + "language": "Bosnian", + "id": "ESBBT644VZ64SSEN", + "bio": "Vivamus id faucibus velit, id posuere leo. Etiam congue dignissim volutpat. Donec lobortis eleifend condimentum. Fusce eu ultrices elit, vel posuere neque.", + "version": 5.59 + }, + { + "name": "Kgosietsile Bogatsu", + "language": "Setswana", + "id": "0B8IOVL2NSVJVV6T", + "bio": "Curabitur quis commodo quam. In id elit malesuada, pulvinar mi eu, imperdiet nulla. Nullam ac sodales dolor, eu facilisis dui. Duis commodo orci ut dolor iaculis facilisis.", + "version": 6.78 + }, + { + "name": "Sushant Bhargav", + "language": "Hindi", + "id": "PRWA7HE1GJ7OCYQM", + "bio": "Proin tempus eu risus nec mattis. Maecenas tempus neque ut porttitor malesuada. Quisque efficitur vel sapien ut imperdiet. Quisque efficitur vel sapien ut imperdiet.", + "version": 5.36 + }, + { + "name": "Monika Nayak", + "language": "Hindi", + "id": "RO0ZCWFTY6MJ66AZ", + "bio": "Sed eu libero maximus nunc lacinia lobortis et sit amet nisi. Quisque efficitur vel sapien ut imperdiet. Nam rutrum sollicitudin ante tempus consequat. Curabitur ultricies id urna nec ultrices. Phasellus massa ligula, hendrerit eget efficitur eget, tincidunt in ligula.", + "version": 7.58 + }, + { + "name": "Guðrún Guðmundsdóttir", + "language": "Icelandic", + "id": "R1TRJT5TWANYO88D", + "bio": "Maecenas non arcu nulla. In sed ultricies lorem.", + "version": 4.65 + }, + { + "name": "Shakti Menon", + "language": "Hindi", + "id": "J1NSHQXRWA7CY0AZ", + "bio": "Vivamus id faucibus velit, id posuere leo. Etiam malesuada blandit erat, nec ultricies leo maximus sed. Nam semper gravida nunc, sit amet elementum ipsum.", + "version": 5.16 + }, + { + "name": "Ndumiso Hlatshwayo", + "language": "isiZulu", + "id": "533XA8H67VO8CSGQ", + "bio": "Quisque efficitur vel sapien ut imperdiet. Nam semper gravida nunc, sit amet elementum ipsum. Donec pellentesque ultrices mi, non consectetur eros luctus non. Vestibulum pharetra libero et velit gravida euismod. Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + "version": 5.24 + }, + { + "name": "Lucky Shastry", + "language": "Hindi", + "id": "3OBF3U08WI1QF63N", + "bio": "Morbi ultricies consequat ligula posuere eleifend. Suspendisse sit amet ullamcorper sem.", + "version": 7.86 + }, + { + "name": "Pule Matlhaku", + "language": "Setswana", + "id": "UPATVXM44DAFUDI7", + "bio": "Maecenas tempus neque ut porttitor malesuada. Vivamus id faucibus velit, id posuere leo. Morbi finibus dui sed est fringilla ornare.", + "version": 4.12 + }, + { + "name": "Raju Rathore", + "language": "Hindi", + "id": "QQMNYP788DEFG4IS", + "bio": "Nam rutrum sollicitudin ante tempus consequat. Pellentesque massa sem, scelerisque sit amet odio id, cursus tempor urna. Integer vehicula, arcu sit amet egestas efficitur, orci justo interdum massa, eget ullamcorper risus ligula tristique libero.", + "version": 9.86 + }, + { + "name": "Xolani Ngcobo", + "language": "isiZulu", + "id": "SXWZ4IYT5VZA6WEE", + "bio": "Ut dictum, ligula eget sagittis maximus, tellus mi varius ex, a accumsan justo tellus vitae leo. Fusce eu ultrices elit, vel posuere neque. Curabitur quis commodo quam.", + "version": 4.77 + }, + { + "name": "Meenakshi Benjaree", + "language": "Hindi", + "id": "933PPBA946YX1K4X", + "bio": "Maecenas tempus neque ut porttitor malesuada. Duis pellentesque ultrices convallis.", + "version": 7.9 + }, + { + "name": "Ólafur Magnússon", + "language": "Icelandic", + "id": "NWY9HV455M3W8QKY", + "bio": "Morbi ultricies consequat ligula posuere eleifend. Duis pellentesque ultrices convallis. Vestibulum pharetra libero et velit gravida euismod. Ut dictum, ligula eget sagittis maximus, tellus mi varius ex, a accumsan justo tellus vitae leo. Aliquam sollicitudin ante ligula, eget malesuada nibh efficitur et.", + "version": 2.09 + }, + { + "name": "Samir Simic", + "language": "Bosnian", + "id": "6H2IO7A62ZVUXGKZ", + "bio": "Etiam malesuada blandit erat, nec ultricies leo maximus sed. Quisque maximus sodales mauris ut elementum. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + "version": 6.93 + }, + { + "name": "Swarnika Soni", + "language": "Hindi", + "id": "4GJF8C6P1Y5RFPMC", + "bio": "Ut maximus, libero nec facilisis fringilla, ex sem sollicitudin leo, non congue tortor ligula in eros. Nunc tincidunt laoreet laoreet.", + "version": 4.82 + }, + { + "name": "Lavanya Mittal", + "language": "Hindi", + "id": "4Z09CO5IJH7CEUD2", + "bio": "Suspendisse sit amet ullamcorper sem. Ut dictum, ligula eget sagittis maximus, tellus mi varius ex, a accumsan justo tellus vitae leo.", + "version": 1.08 + }, + { + "name": "Bontle Mokgatle", + "language": "Setswana", + "id": "4Y497GAOTAFUJDIC", + "bio": "Maecenas non arcu nulla. Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + "version": 1.92 + }, + { + "name": "Prashant Chourey", + "language": "Hindi", + "id": "J4NMMNAALGOIZY8V", + "bio": "Etiam malesuada blandit erat, nec ultricies leo maximus sed. Suspendisse potenti. Phasellus massa ligula, hendrerit eget efficitur eget, tincidunt in ligula. Ut viverra quis eros eu tincidunt.", + "version": 8.59 + }, + { + "name": "Prakash Malviya", + "language": "Hindi", + "id": "P442H9CEHIU6HAFV", + "bio": "Proin tempus eu risus nec mattis. Integer vehicula, arcu sit amet egestas efficitur, orci justo interdum massa, eget ullamcorper risus ligula tristique libero. Vivamus id faucibus velit, id posuere leo. In id elit malesuada, pulvinar mi eu, imperdiet nulla. Donec pellentesque ultrices mi, non consectetur eros luctus non.", + "version": 8.21 + }, + { + "name": "Ivana Kalic", + "language": "Bosnian", + "id": "31VIE8WWDJWKE5YL", + "bio": "Quisque efficitur vel sapien ut imperdiet. Duis luctus, lacus eu aliquet convallis, purus elit malesuada ex, vitae rutrum ipsum dui ut magna.", + "version": 6.99 + }, + { + "name": "Ajeet Vasav", + "language": "Hindi", + "id": "ODNPTWVSRBPII0BH", + "bio": "Aenean finibus in tortor vel aliquet. Integer vehicula, arcu sit amet egestas efficitur, orci justo interdum massa, eget ullamcorper risus ligula tristique libero. Morbi finibus dui sed est fringilla ornare. Morbi finibus dui sed est fringilla ornare. Ut maximus, libero nec facilisis fringilla, ex sem sollicitudin leo, non congue tortor ligula in eros.", + "version": 3.6 + }, + { + "name": "Jóhanna Jóhannsdóttir", + "language": "Icelandic", + "id": "ZI21GM8B08FVLMF0", + "bio": "In sed ultricies lorem. Etiam malesuada blandit erat, nec ultricies leo maximus sed.", + "version": 4.93 + }, + { + "name": "Seema Thapar", + "language": "Hindi", + "id": "IZSO10C5ZHVYQ5O2", + "bio": "Duis commodo orci ut dolor iaculis facilisis. Etiam mauris magna, fermentum vitae aliquet eu, cursus vitae sapien. Maecenas tempus neque ut porttitor malesuada. Phasellus massa ligula, hendrerit eget efficitur eget, tincidunt in ligula. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Etiam consequat enim lorem, at tincidunt velit ultricies et.", + "version": 1.79 + }, + { + "name": "María Stefánsdóttir", + "language": "Icelandic", + "id": "KWH2RVHSB25MYGL9", + "bio": "In id elit malesuada, pulvinar mi eu, imperdiet nulla. Sed eu libero maximus nunc lacinia lobortis et sit amet nisi. Ut viverra quis eros eu tincidunt. Nam rutrum sollicitudin ante tempus consequat.", + "version": 5.21 + }, + { + "name": "Denis Terzic", + "language": "Bosnian", + "id": "1WQO4VGBS2U7DOSL", + "bio": "Ut accumsan, est vel fringilla varius, purus augue blandit nisl, eu rhoncus ligula purus vel dolor. Curabitur quis commodo quam. Curabitur ultricies id urna nec ultrices. Nam rutrum sollicitudin ante tempus consequat. Morbi finibus dui sed est fringilla ornare.", + "version": 6.32 + }, + { + "name": "Ana Livic", + "language": "Bosnian", + "id": "8JYVK7SM07YQOVQ3", + "bio": "Nam tristique feugiat est vitae mollis. Aliquam sollicitudin ante ligula, eget malesuada nibh efficitur et. Pellentesque massa sem, scelerisque sit amet odio id, cursus tempor urna. Proin tempus eu risus nec mattis. Sed eu libero maximus nunc lacinia lobortis et sit amet nisi.", + "version": 5.93 + }, + { + "name": "Bukhosi Bhengu", + "language": "isiZulu", + "id": "AFYXL0UNGMU0B1H2", + "bio": "Curabitur quis commodo quam. Curabitur sed condimentum felis, ut luctus eros. Aliquam scelerisque pretium tellus, sed accumsan est ultrices id. Aliquam scelerisque pretium tellus, sed accumsan est ultrices id. Sed nec suscipit ligula.", + "version": 9.37 + }, + { + "name": "Siyabonga Sithole", + "language": "isiZulu", + "id": "NJDX77JXV51CNGF5", + "bio": "Quisque mauris ligula, efficitur porttitor sodales ac, lacinia non ex. Sed laoreet posuere sapien, ut feugiat nibh gravida at.", + "version": 8.22 + }, + { + "name": "Meena Dubey", + "language": "Hindi", + "id": "GCJGYXSPDEFF9BTN", + "bio": "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Etiam consequat enim lorem, at tincidunt velit ultricies et. Donec lobortis eleifend condimentum. Morbi ac tellus erat. Maecenas quis nisi nunc.", + "version": 2.95 + }, + { + "name": "Chandrika Gupta", + "language": "Hindi", + "id": "7KFJHS86WKTL6Q12", + "bio": "Aliquam sollicitudin ante ligula, eget malesuada nibh efficitur et. Suspendisse sit amet ullamcorper sem. Etiam mauris magna, fermentum vitae aliquet eu, cursus vitae sapien.", + "version": 5.35 + }, + { + "name": "Akhilesh Khare", + "language": "Hindi", + "id": "ATINHMT01VNMMDCP", + "bio": "Donec congue sapien vel euismod interdum. Suspendisse potenti. Nullam ac sodales dolor, eu facilisis dui. Nam tristique feugiat est vitae mollis. Curabitur ultricies id urna nec ultrices.", + "version": 3.68 + }, + { + "name": "Motsumi Basiang", + "language": "Setswana", + "id": "MUELSFQENUOHGBZ3", + "bio": "Cras dictum dolor lacinia lectus vehicula rutrum. Ut maximus, libero nec facilisis fringilla, ex sem sollicitudin leo, non congue tortor ligula in eros. Donec congue sapien vel euismod interdum.", + "version": 5.23 + }, + { + "name": "Neha Benjaree", + "language": "Hindi", + "id": "5VTSZUD0SA9JVL40", + "bio": "Morbi ultricies consequat ligula posuere eleifend. Nulla finibus massa at viverra facilisis. Nam tristique feugiat est vitae mollis.", + "version": 5.73 + }, + { + "name": "Kristín Sigurðardóttir", + "language": "Icelandic", + "id": "ZP5TBBYX6RI2UJ31", + "bio": "Cras dictum dolor lacinia lectus vehicula rutrum. Cras dictum dolor lacinia lectus vehicula rutrum. Duis luctus, lacus eu aliquet convallis, purus elit malesuada ex, vitae rutrum ipsum dui ut magna. Fusce congue aliquam elit ut luctus. Duis commodo orci ut dolor iaculis facilisis.", + "version": 2.8 + }, + { + "name": "Rohini Vasav", + "language": "Hindi", + "id": "UEFML43TCGS04KWM", + "bio": "Ut accumsan, est vel fringilla varius, purus augue blandit nisl, eu rhoncus ligula purus vel dolor. Ut maximus, libero nec facilisis fringilla, ex sem sollicitudin leo, non congue tortor ligula in eros. Nam rutrum sollicitudin ante tempus consequat. Aliquam scelerisque pretium tellus, sed accumsan est ultrices id. Suspendisse sit amet ullamcorper sem.", + "version": 9.3 + }, + { + "name": "Sunil Kapoor", + "language": "Hindi", + "id": "VY2A0APGVHK5NAW2", + "bio": "Proin tempus eu risus nec mattis. Ut dictum, ligula eget sagittis maximus, tellus mi varius ex, a accumsan justo tellus vitae leo. In id elit malesuada, pulvinar mi eu, imperdiet nulla.", + "version": 8.04 + }, + { + "name": "Zamokuhle Zulu", + "language": "isiZulu", + "id": "XU7BX2F8M5PVZ1EF", + "bio": "Etiam congue dignissim volutpat. Phasellus tincidunt sollicitudin posuere. Phasellus tincidunt sollicitudin posuere. Nam tristique feugiat est vitae mollis.", + "version": 8.39 + }, + { + "name": "Bhupesh Menon", + "language": "Hindi", + "id": "0CEPNRDV98KT3ORP", + "bio": "Maecenas tempus neque ut porttitor malesuada. Phasellus massa ligula, hendrerit eget efficitur eget, tincidunt in ligula. Quisque mauris ligula, efficitur porttitor sodales ac, lacinia non ex. Maecenas quis nisi nunc.", + "version": 2.69 + } +] \ No newline at end of file diff --git a/perf-tests/src/main/scala/sttp/tapir/perf/apis/Endpoints.scala b/perf-tests/src/main/scala/sttp/tapir/perf/apis/Endpoints.scala index 9359682160..70e25e52b9 100644 --- a/perf-tests/src/main/scala/sttp/tapir/perf/apis/Endpoints.scala +++ b/perf-tests/src/main/scala/sttp/tapir/perf/apis/Endpoints.scala @@ -5,7 +5,8 @@ import sttp.tapir._ import sttp.tapir.perf.Common._ import sttp.tapir.server.ServerEndpoint import sttp.tapir.server.model.EndpointExtensions._ - +import io.circe.Json +import sttp.tapir.json.circe._ import java.io.File import scala.concurrent.Future @@ -55,6 +56,17 @@ trait Endpoints { body: File => reply(s"Ok [$n], file saved to ${body.toPath}") } + }, + { (n: Int) => + endpoint.post + .in("pathJson" + n.toString) + .in(jsonBody[Json]) + .maxRequestBodyLength(LargeInputSize + 1024L) + .out(stringBody) + .serverLogicSuccess { + body: Json => + reply(s"Ok [$n], file saved to ${body}") + } } ) } diff --git a/perf-tests/src/test/scala/sttp/tapir/perf/Simulations.scala b/perf-tests/src/test/scala/sttp/tapir/perf/Simulations.scala index 77e4a32912..f6b9c63164 100644 --- a/perf-tests/src/test/scala/sttp/tapir/perf/Simulations.scala +++ b/perf-tests/src/test/scala/sttp/tapir/perf/Simulations.scala @@ -9,6 +9,7 @@ import sttp.tapir.perf.Common._ import scala.concurrent.duration._ import scala.util.Random +import scala.io.Source object CommonSimulations { private val baseUrl = "127.0.0.1:8080" @@ -26,6 +27,8 @@ object CommonSimulations { def randomAlphanumByteArray(size: Int): Array[Byte] = Random.alphanumeric.take(size).map(_.toByte).toArray + def randomJson: Array[Byte] = Source.fromResource("64KB.json").getLines().mkString("\n").getBytes() + lazy val constRandomLongBytes = randomByteArray(LargeInputSize) lazy val constRandomLongAlphanumBytes = randomAlphanumByteArray(LargeInputSize) @@ -130,6 +133,23 @@ object CommonSimulations { .protocols(httpProtocol) } + def scenario_post_long_json(routeNumber: Int, histogram: Histogram, warmup: Boolean = false): PopulationBuilder = { + val execHttpPost = + exec( + http(s"${namePrefix(warmup)}HTTP POST /pathJson$routeNumber") + .post(s"/pathJson$routeNumber") + .body(ByteArrayBody(randomJson)) + .header("Content-Type", "application/json") + .check(sessionSaveResponseTime) + ) + .exec(handleLatencyHistogram(histogram, warmup)) + + scenario(s"${namePrefix(warmup)} Repeatedly invoke POST with json body") + .during(duration(warmup))(execHttpPost) + .inject(atOnceUsers(userCount)) + .protocols(httpProtocol) + } + } import CommonSimulations._ @@ -183,6 +203,12 @@ class PostLongStringSimulation extends PerfTestSuiteRunnerSimulation { setUp(warmup.andThen(measurements)): Unit } +class PostLongJsonSimulation extends PerfTestSuiteRunnerSimulation { + val warmup = scenario_post_long_json(0, histogram, warmup = true) + val small = scenario_post_long_json(0, histogram) + setUp(warmup.andThen(small)): Unit +} + /** Based on https://github.com/kamilkloch/websocket-benchmark/ Can't be executed using PerfTestSuiteRunner, see perfTests/README.md */ class WebSocketsSimulation extends Simulation { diff --git a/project/GenerateListOfExamples.scala b/project/GenerateListOfExamples.scala index da3f8b47fb..e2a6526163 100644 --- a/project/GenerateListOfExamples.scala +++ b/project/GenerateListOfExamples.scala @@ -56,7 +56,7 @@ object GenerateListOfExamples { } .mkString(" ") - s"""* [${example.description}]($LinkBase/$relativeLink) $tags""" + s"""* [${example.description}]($LinkBase$relativeLink) $tags""" } // combining all examples in category .mkString("\n") diff --git a/project/Versions.scala b/project/Versions.scala index d4592797ac..a07ffd1f59 100644 --- a/project/Versions.scala +++ b/project/Versions.scala @@ -1,31 +1,31 @@ object Versions { - val http4s = "0.23.28" - val http4sBlazeServer = "0.23.16" - val http4sBlazeClient = "0.23.16" + val http4s = "0.23.30" + val http4sBlazeServer = "0.23.17" + val http4sBlazeClient = "0.23.17" val catsCore = "2.12.0" - val catsEffect = "3.5.4" + val catsEffect = "3.5.7" val circe = "0.14.9" val circeGenericExtras = "0.14.3" val circeYaml = "0.15.2" val helidon = "4.0.10" - val sttp = "3.9.8" + val sttp = "3.10.1" val sttpModel = "1.7.11" - val sttpShared = "1.3.22" - val sttpApispec = "0.11.3" + val sttpShared = "1.4.2" + val sttpApispec = "0.11.4" val akkaHttp = "10.2.10" val akkaStreams = "2.6.20" - val pekkoHttp = "1.0.1" - val pekkoStreams = "1.1.1" - val swaggerUi = "5.17.14" + val pekkoHttp = "1.1.0" + val pekkoStreams = "1.1.2" + val swaggerUi = "5.18.2" val upickle = "3.3.1" val playJson = "3.0.1" val play29Json = "3.0.4" val finatra = "24.2.0" val catbird = "21.12.0" val json4s = "4.0.7" - val metrics4Scala = "4.3.2" - val nettyReactiveStreams = "3.0.2" - val ox = "0.4.0" + val metrics4Scala = "4.3.3" + val nettyReactiveStreams = "3.0.3" + val ox = "0.5.1" val reactiveStreams = "1.0.4" val sprayJson = "1.3.6" val scalaCheck = "1.18.1" @@ -34,37 +34,37 @@ object Versions { val scalaTestPlusScalaCheck = "3.2.19.0" val refined = "0.11.2" val iron = "2.6.0" - val enumeratum = "1.7.4" - val zio = "2.1.9" + val enumeratum = "1.7.5" + val zio = "2.1.13" val zioHttp = "3.0.1" val zioInteropCats = "23.1.0.3" val zioInteropReactiveStreams = "2.0.2" val zioJson = "0.7.3" - val playClient = "3.0.5" + val playClient = "3.0.6" val playServer = "3.0.5" - val play29Client = "2.2.9" - val play29Server = "2.9.5" - val tethys = "0.29.1" - val vertx = "4.5.10" + val play29Client = "2.2.10" + val play29Server = "2.9.6" + val tethys = "0.29.3" + val vertx = "4.5.11" val jsScalaJavaTime = "2.6.0" val nativeScalaJavaTime = "2.6.0" val jwtScala = "10.0.1" val derevo = "0.13.0" val newtype = "0.4.4" val monixNewtype = "0.3.0" - val zioPrelude = "1.0.0-RC31" + val zioPrelude = "1.0.0-RC35" val awsLambdaInterface = "2.6.0" - val armeria = "1.30.1" + val armeria = "1.31.3" val scalaJava8Compat = "1.0.2" val scalaCollectionCompat = "2.12.0" val fs2 = "3.11.0" val decline = "2.4.1" - val quicklens = "1.9.9" - val openTelemetry = "1.42.1" + val quicklens = "1.9.11" + val openTelemetry = "1.45.0" val mockServer = "5.15.0" - val dogstatsdClient = "4.4.2" - val nettyAll = "4.1.113.Final" - val logback = "1.5.8" + val dogstatsdClient = "4.4.3" + val nettyAll = "4.1.116.Final" + val logback = "1.5.13" val slf4j = "2.0.16" - val jsoniter = "2.30.11" + val jsoniter = "2.32.0" } diff --git a/project/build.properties b/project/build.properties index 0b699c3052..e88a0d817d 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.2 +sbt.version=1.10.6 diff --git a/project/plugins.sbt b/project/plugins.sbt index bfd5f408fe..12c258f014 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,7 +1,7 @@ // https://github.com/sbt/sbt/issues/6997 ThisBuild / libraryDependencySchemes += "org.scala-lang.modules" %% "scala-xml" % VersionScheme.Always -val sbtSoftwareMillVersion = "2.0.20" +val sbtSoftwareMillVersion = "2.0.21" addSbtPlugin("com.softwaremill.sbt-softwaremill" % "sbt-softwaremill-common" % sbtSoftwareMillVersion) addSbtPlugin("com.softwaremill.sbt-softwaremill" % "sbt-softwaremill-publish" % sbtSoftwareMillVersion) addSbtPlugin("com.softwaremill.sbt-softwaremill" % "sbt-softwaremill-browser-test-js" % sbtSoftwareMillVersion) @@ -9,17 +9,17 @@ addSbtPlugin("com.softwaremill.sbt-softwaremill" % "sbt-softwaremill-browser-tes addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.4") addSbtPlugin("org.playframework.twirl" % "sbt-twirl" % "2.0.7") addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.6.1") -addSbtPlugin("com.eed3si9n" % "sbt-projectmatrix" % "0.10.0") +addSbtPlugin("com.eed3si9n" % "sbt-projectmatrix" % "0.10.1") addSbtPlugin("org.jetbrains.scala" % "sbt-ide-settings" % "1.1.2") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.17.0") addSbtPlugin("io.spray" % "sbt-revolver" % "0.10.0") -addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.2.0") -addSbtPlugin("io.gatling" % "gatling-sbt" % "4.10.0") -addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.12.0") -addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.5") +addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.3.0") +addSbtPlugin("io.gatling" % "gatling-sbt" % "4.11.1") +addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.13.1") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.6") addSbtPlugin("com.lightbend.akka.grpc" % "sbt-akka-grpc" % "2.1.4") -addSbtPlugin("org.apache.pekko" % "pekko-grpc-sbt-plugin" % "1.0.2") +addSbtPlugin("org.apache.pekko" % "pekko-grpc-sbt-plugin" % "1.1.1") // needed to override the Android flavor of Guava coming from pekko-grpc-sbt-plugin, which causes failures in Scala.JS builds -dependencyOverrides += "com.google.guava" % "guava" % "32.1.2-jre" +dependencyOverrides += "com.google.guava" % "guava" % "33.4.0-jre" addDependencyTreePlugin diff --git a/server/akka-http-server/src/main/scala/sttp/tapir/server/akkahttp/AkkaBodyListener.scala b/server/akka-http-server/src/main/scala/sttp/tapir/server/akkahttp/AkkaBodyListener.scala index 5d1a9e0a1d..3c428c6fa3 100644 --- a/server/akka-http-server/src/main/scala/sttp/tapir/server/akkahttp/AkkaBodyListener.scala +++ b/server/akka-http-server/src/main/scala/sttp/tapir/server/akkahttp/AkkaBodyListener.scala @@ -14,7 +14,7 @@ class AkkaBodyListener(implicit ec: ExecutionContext) extends BodyListener[Futur override def onComplete(body: AkkaResponseBody)(cb: Try[Unit] => Future[Unit]): Future[AkkaResponseBody] = { body match { case ws @ Left(_) => cb(Success(())).map(_ => ws) - case Right(e @ HttpEntity.Empty) => + case Right(e) if e.isKnownEmpty => Future.successful(Right(e)).andThen { case _ => cb(Success(())) } case Right(e: UniversalEntity) => Future.successful( diff --git a/server/akka-http-server/src/test/scala/sttp/tapir/server/akkahttp/AkkaHttpServerTest.scala b/server/akka-http-server/src/test/scala/sttp/tapir/server/akkahttp/AkkaHttpServerTest.scala index db589bce84..b603e61cb8 100644 --- a/server/akka-http-server/src/test/scala/sttp/tapir/server/akkahttp/AkkaHttpServerTest.scala +++ b/server/akka-http-server/src/test/scala/sttp/tapir/server/akkahttp/AkkaHttpServerTest.scala @@ -14,15 +14,19 @@ import sttp.capabilities.akka.AkkaStreams import sttp.client3._ import sttp.client3.akkahttp.AkkaHttpBackend import sttp.model.sse.ServerSentEvent +import sttp.model.Header +import sttp.model.MediaType import sttp.monad.FutureMonad import sttp.monad.syntax._ import sttp.tapir._ import sttp.tapir.server.interceptor._ import sttp.tapir.server.interceptor.metrics.MetricsRequestInterceptor +import sttp.tapir.server.metrics.{EndpointMetric, Metric} import sttp.tapir.server.tests._ import sttp.tapir.tests.{Test, TestSuite} import java.util.UUID +import java.util.concurrent.atomic.AtomicInteger import scala.concurrent.Future import scala.util.Random @@ -150,6 +154,58 @@ class AkkaHttpServerTest extends TestSuite with EitherValues { r.header("Content-Length") shouldBe Some("0") r.header("Transfer-Encoding") shouldBe None } + }, + Test("execute metrics interceptors for empty body and json content type") { + val e = endpoint.post.in(stringBody) + .out(stringBody) + .out(header(Header.contentType(MediaType.ApplicationJson))) + .serverLogicSuccess[Future](body => Future.successful(body)) + + class DummyMetric { + val onRequestCnt = new AtomicInteger(0) + val onEndpointRequestCnt = new AtomicInteger(0) + val onResponseHeadersCnt = new AtomicInteger(0) + val onResponseBodyCnt = new AtomicInteger(0) + } + val metric = new DummyMetric() + val customMetrics: Metric[Future, DummyMetric] = + Metric( + metric = metric, + onRequest = (_, metric, me) => + me.eval { + metric.onRequestCnt.incrementAndGet() + EndpointMetric( + onEndpointRequest = Some((_) => + me.eval(metric.onEndpointRequestCnt.incrementAndGet()), + ), + onResponseHeaders = Some((_, _) => + me.eval(metric.onResponseHeadersCnt.incrementAndGet()), + ), + onResponseBody = Some((_, _) => + me.eval(metric.onResponseBodyCnt.incrementAndGet()), + ), + onException = None, + ) + }, + ) + val route = AkkaHttpServerInterpreter( + AkkaHttpServerOptions.customiseInterceptors + .metricsInterceptor(new MetricsRequestInterceptor[Future](List(customMetrics), Seq.empty)) + .options + ).toRoute(e) + + interpreter + .server(NonEmptyList.of(route)) + .use { port => + basicRequest.post(uri"http://localhost:$port").body("").send(backend).map { response => + response.body shouldBe Right("") + metric.onRequestCnt.get() shouldBe 1 + metric.onEndpointRequestCnt.get() shouldBe 1 + metric.onResponseHeadersCnt.get() shouldBe 1 + metric.onResponseBodyCnt.get() shouldBe 1 + } + } + .unsafeToFuture() } ) def drainAkka(stream: AkkaStreams.BinaryStream): Future[Unit] = diff --git a/server/armeria-server/src/main/scala/sttp/tapir/server/armeria/ArmeriaServerRequest.scala b/server/armeria-server/src/main/scala/sttp/tapir/server/armeria/ArmeriaServerRequest.scala index ec2190ebfa..bb95fd9e66 100644 --- a/server/armeria-server/src/main/scala/sttp/tapir/server/armeria/ArmeriaServerRequest.scala +++ b/server/armeria-server/src/main/scala/sttp/tapir/server/armeria/ArmeriaServerRequest.scala @@ -38,12 +38,8 @@ private[armeria] final case class ArmeriaServerRequest(ctx: ServiceRequestContex override def underlying: Any = ctx override val pathSegments: List[String] = { - // ctx.path() always starts with '/'. - if (ctx.path() == "/") { - Nil - } else { - ctx.path().substring(1).split("/").toList - } + val segments = uri.pathSegments.segments.map(_.v).filter(_.nonEmpty).toList + if (segments == List("")) Nil else segments // representing the root path as an empty list } override val queryParameters: QueryParams = { diff --git a/server/core/src/main/scala/sttp/tapir/server/interceptor/metrics/MetricsEndpointInterceptor.scala b/server/core/src/main/scala/sttp/tapir/server/interceptor/metrics/MetricsEndpointInterceptor.scala index 57f8fc047d..069232e153 100644 --- a/server/core/src/main/scala/sttp/tapir/server/interceptor/metrics/MetricsEndpointInterceptor.scala +++ b/server/core/src/main/scala/sttp/tapir/server/interceptor/metrics/MetricsEndpointInterceptor.scala @@ -47,7 +47,7 @@ private[metrics] class MetricsEndpointInterceptor[F[_]]( )(implicit monad: MonadError[F], bodyListener: BodyListener[F, B]): F[ServerResponse[B]] = { if (ignoreEndpoints.contains(ctx.endpoint)) endpointHandler.onDecodeSuccess(ctx) else { - val responseWithMetrics: F[ServerResponse[B]] = for { + def responseWithMetrics: F[ServerResponse[B]] = for { _ <- collectRequestMetrics(ctx.endpoint) response <- endpointHandler.onDecodeSuccess(ctx) _ <- collectResponseHeadersMetrics(ctx.endpoint, response) @@ -64,7 +64,7 @@ private[metrics] class MetricsEndpointInterceptor[F[_]]( )(implicit monad: MonadError[F], bodyListener: BodyListener[F, B]): F[ServerResponse[B]] = { if (ignoreEndpoints.contains(ctx.endpoint)) endpointHandler.onSecurityFailure(ctx) else { - val responseWithMetrics: F[ServerResponse[B]] = for { + def responseWithMetrics: F[ServerResponse[B]] = for { _ <- collectRequestMetrics(ctx.endpoint) response <- endpointHandler.onSecurityFailure(ctx) _ <- collectResponseHeadersMetrics(ctx.endpoint, response) @@ -83,7 +83,7 @@ private[metrics] class MetricsEndpointInterceptor[F[_]]( )(implicit monad: MonadError[F], bodyListener: BodyListener[F, B]): F[Option[ServerResponse[B]]] = { if (ignoreEndpoints.contains(ctx.endpoint)) endpointHandler.onDecodeFailure(ctx) else { - val responseWithMetrics: F[Option[ServerResponse[B]]] = for { + def responseWithMetrics: F[Option[ServerResponse[B]]] = for { response <- endpointHandler.onDecodeFailure(ctx) withMetrics <- response match { case Some(response) => @@ -129,7 +129,7 @@ private[metrics] class MetricsEndpointInterceptor[F[_]]( } } - private def handleResponseExceptions[T](r: F[T], e: AnyEndpoint)(implicit monad: MonadError[F]): F[T] = + private def handleResponseExceptions[T](r: => F[T], e: AnyEndpoint)(implicit monad: MonadError[F]): F[T] = r.handleError { case ex: Exception => collectExceptionMetrics(e, ex) } private def collectExceptionMetrics[T](e: AnyEndpoint, ex: Throwable)(implicit monad: MonadError[F]): F[T] = diff --git a/server/core/src/main/scala/sttp/tapir/server/metrics/Metric.scala b/server/core/src/main/scala/sttp/tapir/server/metrics/Metric.scala index 8c90e9fb3b..82c32bff01 100644 --- a/server/core/src/main/scala/sttp/tapir/server/metrics/Metric.scala +++ b/server/core/src/main/scala/sttp/tapir/server/metrics/Metric.scala @@ -29,15 +29,15 @@ case class EndpointMetric[F[_]]( case class ResponsePhaseLabel(name: String, headersValue: String, bodyValue: String) case class MetricLabels( forRequest: List[(String, (AnyEndpoint, ServerRequest) => String)], - forResponse: List[(String, Either[Throwable, ServerResponse[_]] => String)], + forResponse: List[(String, Either[Throwable, ServerResponse[_]] => Option[String])], forResponsePhase: ResponsePhaseLabel = ResponsePhaseLabel("phase", "headers", "body") ) { def namesForRequest: List[String] = forRequest.map { case (name, _) => name } def namesForResponse: List[String] = forResponse.map { case (name, _) => name } def valuesForRequest(ep: AnyEndpoint, req: ServerRequest): List[String] = forRequest.map { case (_, f) => f(ep, req) } - def valuesForResponse(res: ServerResponse[_]): List[String] = forResponse.map { case (_, f) => f(Right(res)) } - def valuesForResponse(ex: Throwable): List[String] = forResponse.map { case (_, f) => f(Left(ex)) } + def valuesForResponse(res: ServerResponse[_]): List[String] = forResponse.flatMap { case (_, f) => f(Right(res)).toList } + def valuesForResponse(ex: Throwable): List[String] = forResponse.flatMap { case (_, f) => f(Left(ex)).toList } } object MetricLabels { @@ -51,15 +51,15 @@ object MetricLabels { forResponse = List( "status" -> { case Right(r) => - r.code match { + Some(r.code match { case c if c.isInformational => "1xx" case c if c.isSuccess => "2xx" case c if c.isRedirect => "3xx" case c if c.isClientError => "4xx" case c if c.isServerError => "5xx" case _ => "" - } - case Left(_) => "5xx" + }) + case Left(_) => Some("5xx") } ) ) diff --git a/server/netty-server/cats/src/main/scala/sttp/tapir/server/netty/cats/internal/Fs2StreamCompatible.scala b/server/netty-server/cats/src/main/scala/sttp/tapir/server/netty/cats/internal/Fs2StreamCompatible.scala index 8f257f3559..cd870ece82 100644 --- a/server/netty-server/cats/src/main/scala/sttp/tapir/server/netty/cats/internal/Fs2StreamCompatible.scala +++ b/server/netty-server/cats/src/main/scala/sttp/tapir/server/netty/cats/internal/Fs2StreamCompatible.scala @@ -52,9 +52,14 @@ object Fs2StreamCompatible { .flatMap(s => s.sub.stream(Sync[F].delay(publisher.subscribe(s)))) .flatMap(httpContent => fs2.Stream.chunk { - val fs2Chunk = Chunk.byteBuffer(httpContent.content.nioBuffer()) - httpContent.release() // https://netty.io/wiki/reference-counted-objects.html - fs2Chunk + // #4194: we need to copy the data here as we don't know when the data will be ultimately read, and hence + // when we'll be able to release the Netty buffer + val buf = httpContent.content.nioBuffer() + try { + val content = new Array[Byte](buf.remaining()) + buf.get(content) + Chunk.array(content) + } finally { val _ = httpContent.release() } // https://netty.io/wiki/reference-counted-objects.html } ) maxBytes.map(Fs2Streams.limitBytes(stream, _)).getOrElse(stream) diff --git a/server/netty-server/src/main/scala/sttp/tapir/server/netty/NettyConfig.scala b/server/netty-server/src/main/scala/sttp/tapir/server/netty/NettyConfig.scala index dc799c362a..c8eee8395b 100644 --- a/server/netty-server/src/main/scala/sttp/tapir/server/netty/NettyConfig.scala +++ b/server/netty-server/src/main/scala/sttp/tapir/server/netty/NettyConfig.scala @@ -104,8 +104,8 @@ case class NettyConfig( def initPipeline(f: NettyConfig => (ChannelPipeline, ChannelHandler) => Unit): NettyConfig = copy(initPipeline = f) - def withGracefulShutdownTimeout(t: FiniteDuration) = copy(gracefulShutdownTimeout = Some(t)) - def noGracefulShutdown = copy(gracefulShutdownTimeout = None) + def withGracefulShutdownTimeout(t: FiniteDuration): NettyConfig = copy(gracefulShutdownTimeout = Some(t)) + def noGracefulShutdown: NettyConfig = copy(gracefulShutdownTimeout = None) def serverHeader(h: String): NettyConfig = copy(serverHeader = Some(h)) diff --git a/server/netty-server/src/main/scala/sttp/tapir/server/netty/internal/NettyServerHandler.scala b/server/netty-server/src/main/scala/sttp/tapir/server/netty/internal/NettyServerHandler.scala index 6183072e0f..40ffe282dc 100644 --- a/server/netty-server/src/main/scala/sttp/tapir/server/netty/internal/NettyServerHandler.scala +++ b/server/netty-server/src/main/scala/sttp/tapir/server/netty/internal/NettyServerHandler.scala @@ -30,6 +30,9 @@ import scala.collection.mutable.{Queue => MutableQueue} import scala.concurrent.{ExecutionContext, Future} import scala.util.control.NonFatal import scala.util.{Failure, Success} +import java.util.concurrent.TimeoutException +import org.reactivestreams.Subscriber +import org.reactivestreams.Subscription /** @param unsafeRunAsync * Function which dispatches given effect to run asynchronously, returning its result as a Future, and function of type `() => @@ -109,11 +112,13 @@ class NettyServerHandler[F[_]]( evt match { case e: IdleStateEvent => if (e.state() == IdleState.WRITER_IDLE) { - logger.error(s"Closing connection due to exceeded response timeout of ${config.requestTimeout}") + logger.error( + s"Closing connection due to exceeded response timeout of ${config.requestTimeout.map(_.toString).getOrElse("(not set)")}" + ) writeError503ThenClose(ctx) } if (e.state() == IdleState.ALL_IDLE) { - logger.debug(s"Closing connection due to exceeded idle timeout of ${config.idleTimeout}") + logger.debug(s"Closing connection due to exceeded idle timeout of ${config.idleTimeout.map(_.toString).getOrElse("(not set)")}") val _ = ctx.close() } case other => @@ -147,30 +152,42 @@ class NettyServerHandler[F[_]]( pendingResponses.enqueue(cancellationSwitch) lastResponseSent = lastResponseSent.flatMap { _ => runningFuture - .andThen { case _ => - requestTimeoutHandler.foreach(ctx.pipeline().remove) - }(eventLoopContext) - .transform { - case Success(serverResponse) => - pendingResponses.dequeue() - try { - handleResponse(ctx, req, serverResponse) - Success(()) - } catch { - case NonFatal(ex) => - writeError500(req, ex) - Failure(ex) - } finally { - val _ = releaseReq() - } - case Failure(NonFatal(ex)) => - try { - writeError500(req, ex) - Failure(ex) - } finally { - val _ = releaseReq() + .transform { result => + try { + // #4131: the channel might be closed if the request timed out + // both timeout & response-ready events (i.e., comleting this future) are handled on the event loop's executor, + // so they won't be handled concurrently + if (ctx.channel().isOpen()) { + requestTimeoutHandler.foreach(ctx.pipeline().remove) + result match { + case Success(serverResponse) => + pendingResponses.dequeue() + try { + handleResponse(ctx, req, serverResponse) + Success(()) + } catch { + case NonFatal(ex) => + writeError500(req, ex) + Failure(ex) + } + case Failure(NonFatal(ex)) => + writeError500(req, ex) + Failure(ex) + case Failure(fatalException) => Failure(fatalException) + } + } else { + // pendingResponses is already dequeued because the channel is closed + result match { + case Success(serverResponse) => + val e = new TimeoutException("Request timed out") + handleResponseAfterTimeout(ctx, serverResponse, e) + Failure(e) + case Failure(e) => Failure(e) + } } - case Failure(fatalException) => Failure(fatalException) + } finally { + val _ = releaseReq() + } }(eventLoopContext) }(eventLoopContext) } @@ -270,6 +287,39 @@ class NettyServerHandler[F[_]]( } ) + private def handleResponseAfterTimeout( + ctx: ChannelHandlerContext, + serverResponse: ServerResponse[NettyResponse], + timeoutException: Exception + ): Unit = + serverResponse.handle( + ctx = ctx, + byteBufHandler = (channelPromise, byteBuf) => { val _ = channelPromise.setFailure(timeoutException) }, + chunkedStreamHandler = (channelPromise, chunkedStream) => { + chunkedStream.close() + val _ = channelPromise.setFailure(timeoutException) + }, + chunkedFileHandler = (channelPromise, chunkedFile) => { + chunkedFile.close() + val _ = channelPromise.setFailure(timeoutException) + }, + reactiveStreamHandler = (channelPromise, publisher) => { + publisher.subscribe(new Subscriber[HttpContent] { + override def onSubscribe(s: Subscription): Unit = { + s.cancel() + val _ = channelPromise.setFailure(timeoutException) + } + override def onNext(t: HttpContent): Unit = () + override def onError(t: Throwable): Unit = () + override def onComplete(): Unit = () + }) + }, + wsHandler = (responseContent) => { + val _ = responseContent.channelPromise.setFailure(timeoutException) + }, + noBodyHandler = () => () + ) + private def initWsPipeline( ctx: ChannelHandlerContext, r: ReactiveWebSocketProcessorNettyResponseContent, diff --git a/server/netty-server/src/test/scala/sttp/tapir/server/netty/NettyFutureRequestTimeoutTests.scala b/server/netty-server/src/test/scala/sttp/tapir/server/netty/NettyFutureRequestTimeoutTests.scala new file mode 100644 index 0000000000..d65308176c --- /dev/null +++ b/server/netty-server/src/test/scala/sttp/tapir/server/netty/NettyFutureRequestTimeoutTests.scala @@ -0,0 +1,81 @@ +package sttp.tapir.server.netty + +import sttp.tapir._ +import sttp.tapir.tests.Test +import scala.concurrent.Future +import java.util.concurrent.atomic.AtomicInteger +import scala.concurrent.duration.DurationInt +import sttp.tapir.server.interceptor.metrics.MetricsRequestInterceptor +import sttp.tapir.server.metrics.Metric +import sttp.tapir.server.metrics.EndpointMetric +import io.netty.channel.EventLoopGroup +import cats.effect.IO +import cats.effect.kernel.Resource +import scala.concurrent.ExecutionContext +import sttp.client3._ +import sttp.capabilities.fs2.Fs2Streams +import sttp.capabilities.WebSockets +import org.scalatest.matchers.should.Matchers._ +import cats.effect.unsafe.implicits.global +import sttp.model.StatusCode + +class NettyFutureRequestTimeoutTests(eventLoopGroup: EventLoopGroup, backend: SttpBackend[IO, Fs2Streams[IO] with WebSockets])(implicit + ec: ExecutionContext +) { + def tests(): List[Test] = List( + Test("properly update metrics when a request times out") { + val e = endpoint.post + .in(stringBody) + .out(stringBody) + .serverLogicSuccess[Future] { body => + Thread.sleep(2000); Future.successful(body) + } + + val activeRequests = new AtomicInteger() + val totalRequests = new AtomicInteger() + val customMetrics: List[Metric[Future, AtomicInteger]] = List( + Metric( + metric = activeRequests, + onRequest = (_, metric, me) => + me.eval { + EndpointMetric() + .onEndpointRequest { _ => me.eval { val _ = metric.incrementAndGet(); } } + .onResponseBody { (_, _) => me.eval { val _ = metric.decrementAndGet(); } } + .onException { (_, _) => me.eval { val _ = metric.decrementAndGet(); } } + } + ), + Metric( + metric = totalRequests, + onRequest = (_, metric, me) => me.eval(EndpointMetric().onEndpointRequest { _ => me.eval { val _ = metric.incrementAndGet(); } }) + ) + ) + + val config = + NettyConfig.default + .eventLoopGroup(eventLoopGroup) + .randomPort + .withDontShutdownEventLoopGroupOnClose + .noGracefulShutdown + .requestTimeout(1.second) + val options = NettyFutureServerOptions.customiseInterceptors + .metricsInterceptor(new MetricsRequestInterceptor[Future](customMetrics, Seq.empty)) + .options + val bind = IO.fromFuture(IO.delay(NettyFutureServer(options, config).addEndpoints(List(e)).start())) + + Resource + .make(bind)(server => IO.fromFuture(IO.delay(server.stop()))) + .map(_.port) + .use { port => + basicRequest.post(uri"http://localhost:$port").body("test").send(backend).map { response => + response.body should matchPattern { case Left(_) => } + response.code shouldBe StatusCode.ServiceUnavailable + // the metrics will only be updated when the endpoint's logic completes, which is 1 second after receiving the timeout response + Thread.sleep(1100) + activeRequests.get() shouldBe 0 + totalRequests.get() shouldBe 1 + } + } + .unsafeToFuture() + } + ) +} diff --git a/server/netty-server/src/test/scala/sttp/tapir/server/netty/NettyFutureServerTest.scala b/server/netty-server/src/test/scala/sttp/tapir/server/netty/NettyFutureServerTest.scala index f8f263706e..401940ff69 100644 --- a/server/netty-server/src/test/scala/sttp/tapir/server/netty/NettyFutureServerTest.scala +++ b/server/netty-server/src/test/scala/sttp/tapir/server/netty/NettyFutureServerTest.scala @@ -23,7 +23,8 @@ class NettyFutureServerTest extends TestSuite with EitherValues { val tests = new AllServerTests(createServerTest, interpreter, backend, multipart = false).tests() ++ - new ServerGracefulShutdownTests(createServerTest, Sleeper.futureSleeper).tests() + new ServerGracefulShutdownTests(createServerTest, Sleeper.futureSleeper).tests() ++ + new NettyFutureRequestTimeoutTests(eventLoopGroup, backend).tests() (tests, eventLoopGroup) }) { case (_, eventLoopGroup) => diff --git a/server/netty-server/sync/src/main/scala/sttp/tapir/server/netty/sync/NettyOxStreams.scala b/server/netty-server/sync/src/main/scala/sttp/tapir/server/netty/sync/NettyOxStreams.scala index 498dbf4b36..ef52aaddc1 100644 --- a/server/netty-server/sync/src/main/scala/sttp/tapir/server/netty/sync/NettyOxStreams.scala +++ b/server/netty-server/sync/src/main/scala/sttp/tapir/server/netty/sync/NettyOxStreams.scala @@ -1,11 +1,10 @@ package sttp.tapir.server.netty.sync -import ox.Ox -import ox.channels.Source +import ox.flow.Flow import sttp.capabilities.Streams trait OxStreams extends Streams[OxStreams]: override type BinaryStream = Nothing - override type Pipe[A, B] = Ox ?=> Source[A] => Source[B] + override type Pipe[A, B] = Flow[A] => Flow[B] object OxStreams extends OxStreams diff --git a/server/netty-server/sync/src/main/scala/sttp/tapir/server/netty/sync/NettySyncServer.scala b/server/netty-server/sync/src/main/scala/sttp/tapir/server/netty/sync/NettySyncServer.scala index 70ae9f9f41..aef603848d 100644 --- a/server/netty-server/sync/src/main/scala/sttp/tapir/server/netty/sync/NettySyncServer.scala +++ b/server/netty-server/sync/src/main/scala/sttp/tapir/server/netty/sync/NettySyncServer.scala @@ -20,6 +20,7 @@ import java.util.concurrent.{Executors, Future => JFuture} import scala.concurrent.duration.FiniteDuration import scala.concurrent.{Future, Promise} import scala.util.control.NonFatal +import org.slf4j.LoggerFactory /** Unlike with most typical Tapir backends, adding endpoints doesn't immediatly convert them to a Route, because creating a Route requires * providing an Ox concurrency scope. Instead, it stores Endpoints and defers route creation until server.start() is called. This internal @@ -38,6 +39,7 @@ case class NettySyncServer( config: NettyConfig ): private val executor = Executors.newVirtualThreadPerTaskExecutor() + private val logger = LoggerFactory.getLogger(getClass.getName) def addEndpoint(se: ServerEndpoint[OxStreams & WebSockets, Identity]): NettySyncServer = addEndpoints(List(se)) def addEndpoint(se: ServerEndpoint[OxStreams & WebSockets, Identity], overrideOptions: NettySyncServerOptions): NettySyncServer = @@ -71,7 +73,8 @@ case class NettySyncServer( def start()(using Ox): NettySyncServerBinding = startUsingSocketOverride[InetSocketAddress](None, OxDispatcher.create) match case (socket, stop) => - NettySyncServerBinding(socket, stop) + NettySyncServerBinding(socket, stop).tap: binding => + logger.info(s"Tapir Netty server started on ${binding.hostName}:${binding.port}") /** Starts the server and blocks current virtual thread. Ensures graceful shutdown if the running server gets interrupted. Use [[start]] * if you need to manually control concurrency scope or server lifecycle. diff --git a/server/netty-server/sync/src/main/scala/sttp/tapir/server/netty/sync/NettySyncServerOptions.scala b/server/netty-server/sync/src/main/scala/sttp/tapir/server/netty/sync/NettySyncServerOptions.scala index 097c20b24e..1d69fa3e11 100644 --- a/server/netty-server/sync/src/main/scala/sttp/tapir/server/netty/sync/NettySyncServerOptions.scala +++ b/server/netty-server/sync/src/main/scala/sttp/tapir/server/netty/sync/NettySyncServerOptions.scala @@ -47,7 +47,12 @@ object NettySyncServerOptions: doLogWhenReceived = debugLog(_, None), doLogWhenHandled = debugLog, doLogAllDecodeFailures = debugLog, - doLogExceptions = (msg: String, ex: Throwable) => log.error(msg, ex), + doLogExceptions = (msg: String, e: Throwable) => + e match + // if server logic is interrupted (e.g. due to timeout), this isn't an error, but might still be useful for debugging, + // to know how far processing got + case _: InterruptedException => log.debug(msg, e) + case _ => log.error(msg, e), noLog = () ) diff --git a/server/netty-server/sync/src/main/scala/sttp/tapir/server/netty/sync/internal/NettySyncToResponseBody.scala b/server/netty-server/sync/src/main/scala/sttp/tapir/server/netty/sync/internal/NettySyncToResponseBody.scala index 57bd6673a7..4b49c2ec4b 100644 --- a/server/netty-server/sync/src/main/scala/sttp/tapir/server/netty/sync/internal/NettySyncToResponseBody.scala +++ b/server/netty-server/sync/src/main/scala/sttp/tapir/server/netty/sync/internal/NettySyncToResponseBody.scala @@ -1,6 +1,5 @@ package sttp.tapir.server.netty.sync.internal -import _root_.ox.* import io.netty.channel.ChannelHandlerContext import sttp.capabilities import sttp.model.HasHeaders diff --git a/server/netty-server/sync/src/main/scala/sttp/tapir/server/netty/sync/internal/reactivestreams/OxProcessor.scala b/server/netty-server/sync/src/main/scala/sttp/tapir/server/netty/sync/internal/reactivestreams/OxProcessor.scala index c5a1ba09b5..e26cdecd31 100644 --- a/server/netty-server/sync/src/main/scala/sttp/tapir/server/netty/sync/internal/reactivestreams/OxProcessor.scala +++ b/server/netty-server/sync/src/main/scala/sttp/tapir/server/netty/sync/internal/reactivestreams/OxProcessor.scala @@ -10,6 +10,7 @@ import sttp.tapir.server.netty.sync.internal.ox.OxDispatcher import scala.concurrent.duration.* import scala.concurrent.{Await, Future} import scala.util.control.NonFatal +import ox.flow.Flow /** A reactive Processor, which is both a Publisher and a Subscriber * @@ -65,10 +66,7 @@ private[sync] class OxProcessor[A, B]( if subscriber == null then throw new NullPointerException("Subscriber cannot be null") val wrappedSubscriber = wrapSubscriber(subscriber) pipelineForkFuture = oxDispatcher.runAsync { - val outgoingResponses: Source[B] = pipeline((channel: Source[A]).mapAsView { e => - requestsSubscription.request(1) - e - }) + val outgoingResponses: Source[B] = pipeline(Flow.fromSource(channel).tap(_ => requestsSubscription.request(1))).runToChannel() val channelSubscription = new ChannelSubscription(wrappedSubscriber, outgoingResponses) subscriber.onSubscribe(channelSubscription) channelSubscription.runBlocking() // run the main loop which reads from the channel if there's demand @@ -81,7 +79,7 @@ private[sync] class OxProcessor[A, B]( if (pipelineForkFuture != null) try { val pipelineFork = Await.result(pipelineForkFuture, pipelineCancelationTimeout) oxDispatcher.runAsync { - race( + raceSuccess( { ox.sleep(pipelineCancelationTimeout) logger.error(s"Pipeline fork cancelation did not complete in time ($pipelineCancelationTimeout).") diff --git a/server/netty-server/sync/src/main/scala/sttp/tapir/server/netty/sync/internal/ws/OxSourceWebSocketProcessor.scala b/server/netty-server/sync/src/main/scala/sttp/tapir/server/netty/sync/internal/ws/OxSourceWebSocketProcessor.scala index b66eb99bd4..3be240d77d 100644 --- a/server/netty-server/sync/src/main/scala/sttp/tapir/server/netty/sync/internal/ws/OxSourceWebSocketProcessor.scala +++ b/server/netty-server/sync/src/main/scala/sttp/tapir/server/netty/sync/internal/ws/OxSourceWebSocketProcessor.scala @@ -5,7 +5,7 @@ import io.netty.handler.codec.http.websocketx.{CloseWebSocketFrame, WebSocketClo import org.reactivestreams.{Processor, Subscriber, Subscription} import org.slf4j.LoggerFactory import ox.* -import ox.channels.{ChannelClosedException, Source} +import ox.channels.ChannelClosedException import sttp.tapir.model.WebSocketFrameDecodeFailure import sttp.tapir.server.netty.internal.ws.WebSocketFrameConverters.* import sttp.tapir.server.netty.sync.OxStreams @@ -18,6 +18,7 @@ import java.io.IOException import java.util.concurrent.Semaphore import scala.concurrent.duration.* +import ox.flow.Flow private[sync] object OxSourceWebSocketProcessor: private val logger = LoggerFactory.getLogger(getClass.getName) @@ -34,27 +35,20 @@ private[sync] object OxSourceWebSocketProcessor: case x: DecodeResult.Value[REQ] @unchecked => x.v } - val frame2FramePipe: OxStreams.Pipe[NettyWebSocketFrame, NettyWebSocketFrame] = ox ?=> + val frame2FramePipe: OxStreams.Pipe[NettyWebSocketFrame, NettyWebSocketFrame] = incoming => val closeSignal = new Semaphore(0) - (incoming: Source[NettyWebSocketFrame]) => - val outgoing = incoming - .mapAsView { f => - val sttpFrame = nettyFrameToFrame(f) - f.release() - sttpFrame - } - .pipe(takeUntilCloseFrame(passAlongCloseFrame = o.decodeCloseRequests, closeSignal)) - .pipe(optionallyConcatenateFrames(o.concatenateFragmentedFrames)) - .mapAsView(decodeFrame) - .pipe(processingPipe) - .mapAsView(r => frameToNettyFrame(o.responses.encode(r))) - - // when the client closes the connection, we need to close the outgoing channel as well - this needs to be - // done in the client's pipeline code; monitoring that this happens within a timeout after the close happens - monitorOutgoingClosedAfterClientClose(closeSignal, outgoing) - - outgoing - end frame2FramePipe + incoming + .map { f => + val sttpFrame = nettyFrameToFrame(f) + f.release() + sttpFrame + } + .pipe(takeUntilCloseFrame(passAlongCloseFrame = o.decodeCloseRequests, closeSignal)) + .pipe(optionallyConcatenateFrames(o.concatenateFragmentedFrames)) + .map(decodeFrame) + .pipe(processingPipe) + .pipe(monitorOutgoingClosedAfterClientClose(closeSignal)) + .map(r => frameToNettyFrame(o.responses.encode(r))) // We need this kind of interceptor to make Netty reply correctly to closed channel or error def wrapSubscriberWithNettyCallback[B](sub: Subscriber[? >: B]): Subscriber[? >: B] = new Subscriber[B] { @@ -76,31 +70,37 @@ private[sync] object OxSourceWebSocketProcessor: new OxProcessor(oxDispatcher, frame2FramePipe, wrapSubscriberWithNettyCallback) end apply - private def optionallyConcatenateFrames(doConcatenate: Boolean)(s: Source[WebSocketFrame])(using Ox): Source[WebSocketFrame] = - if doConcatenate then s.mapStateful(() => None: Accumulator)(accumulateFrameState).collectAsView { case Some(f: WebSocketFrame) => f } - else s + private def optionallyConcatenateFrames(doConcatenate: Boolean)(f: Flow[WebSocketFrame]): Flow[WebSocketFrame] = + if doConcatenate then f.mapStateful(() => None: Accumulator)(accumulateFrameState).collect { case Some(f: WebSocketFrame) => f } + else f - private def takeUntilCloseFrame(passAlongCloseFrame: Boolean, closeSignal: Semaphore)( - s: Source[WebSocketFrame] - )(using Ox): Source[WebSocketFrame] = - s.takeWhile( + private def takeUntilCloseFrame(passAlongCloseFrame: Boolean, closeSignal: Semaphore)(f: Flow[WebSocketFrame]): Flow[WebSocketFrame] = + f.takeWhile( { case _: WebSocketFrame.Close => closeSignal.release(); false - case _ => true + case f => true }, includeFirstFailing = passAlongCloseFrame ) - private def monitorOutgoingClosedAfterClientClose(closeSignal: Semaphore, outgoing: Source[_])(using Ox): Unit = - // will be interrupted when outgoing is completed - fork { - closeSignal.acquire() - sleep(outgoingCloseAfterCloseTimeout) - if !outgoing.isClosedForReceive then - logger.error( - s"WebSocket outgoing messages channel either not drained, or not closed, " + - s"$outgoingCloseAfterCloseTimeout after receiving a close frame from the client! " + - s"Make sure to complete the outgoing channel in your pipeline, once the incoming " + - s"channel is done!" - ) - }.discard + private def monitorOutgoingClosedAfterClientClose[T](closeSignal: Semaphore)(outgoing: Flow[T]): Flow[T] = + // when the client closes the connection, the outgoing flow has to be completed as well, in the client's pipeline + // code; monitoring that this happens within a timeout after the close happens + Flow.usingEmit { emit => + unsupervised { + forkUnsupervised { + // after the close frame is received from the client, waiting for the given grace period for the flow to + // complete. This will end this scope, and interrupt the `sleep`. If this doesn't happen, logging an error. + closeSignal.acquire() + sleep(outgoingCloseAfterCloseTimeout) + logger.error( + s"WebSocket outgoing messages flow either not drained, or not closed, " + + s"$outgoingCloseAfterCloseTimeout after receiving a close frame from the client! " + + s"Make sure to complete the outgoing flow in your pipeline, once the incoming " + + s"flow is done!" + ) + } + + outgoing.runToEmit(emit) + } + } diff --git a/server/netty-server/sync/src/test/scala/sttp/tapir/server/netty/sync/NettySyncRequestTimeoutTests.scala b/server/netty-server/sync/src/test/scala/sttp/tapir/server/netty/sync/NettySyncRequestTimeoutTests.scala new file mode 100644 index 0000000000..65769d9ba6 --- /dev/null +++ b/server/netty-server/sync/src/test/scala/sttp/tapir/server/netty/sync/NettySyncRequestTimeoutTests.scala @@ -0,0 +1,87 @@ +package sttp.tapir.server.netty + +import cats.effect.IO +import cats.effect.unsafe.implicits.global +import io.netty.channel.EventLoopGroup +import org.scalatest.matchers.should.Matchers.* +import ox.* +import sttp.capabilities.WebSockets +import sttp.capabilities.fs2.Fs2Streams +import sttp.client3.* +import sttp.model.StatusCode +import sttp.tapir.* +import sttp.tapir.server.interceptor.metrics.MetricsRequestInterceptor +import sttp.tapir.server.metrics.{EndpointMetric, Metric} +import sttp.tapir.server.netty.sync.{NettySyncServer, NettySyncServerOptions} +import sttp.tapir.tests.Test + +import java.util.concurrent.atomic.AtomicInteger +import scala.concurrent.Future +import scala.concurrent.duration.DurationInt +import org.slf4j.LoggerFactory + +class NettySyncRequestTimeoutTests(eventLoopGroup: EventLoopGroup, backend: SttpBackend[IO, Fs2Streams[IO] with WebSockets]): + val logger = LoggerFactory.getLogger(getClass.getName) + + def tests(): List[Test] = List( + Test("properly update metrics when a request times out") { + val e = endpoint.post + .in(stringBody) + .out(stringBody) + .serverLogicSuccess[Identity]: body => + Thread.sleep(2000) + body + + val activeRequests = new AtomicInteger() + val totalRequests = new AtomicInteger() + val customMetrics: List[Metric[Identity, AtomicInteger]] = List( + Metric( + metric = activeRequests, + onRequest = (_, metric, me) => + me.eval: + EndpointMetric() + .onEndpointRequest: _ => + val _ = metric.incrementAndGet(); + (): Identity[Unit] + .onResponseBody: (_, _) => + val _ = metric.decrementAndGet(); + .onException: (_, _) => + val _ = metric.decrementAndGet(); + ), + Metric( + metric = totalRequests, + onRequest = (_, metric, me) => + me.eval(EndpointMetric().onEndpointRequest: _ => + val _ = metric.incrementAndGet(); + ) + ) + ) + + val config = + NettyConfig.default + .eventLoopGroup(eventLoopGroup) + .randomPort + .withDontShutdownEventLoopGroupOnClose + .noGracefulShutdown + .requestTimeout(1.second) + val options = NettySyncServerOptions.customiseInterceptors + .metricsInterceptor(new MetricsRequestInterceptor[Identity](customMetrics, Seq.empty)) + .options + + Future.successful: + supervised: + val port = useInScope(NettySyncServer(options, config).addEndpoint(e).start())(_.stop()).port + basicRequest + .post(uri"http://localhost:$port") + .body("test") + .send(backend) + .map: response => + response.body should matchPattern { case Left(_) => } + response.code shouldBe StatusCode.ServiceUnavailable + // unlike in NettyFutureRequestTimeoutTest, here interruption works properly, and the metrics should be updated quickly + Thread.sleep(100) + activeRequests.get() shouldBe 0 + totalRequests.get() shouldBe 1 + .unsafeRunSync() + } + ) diff --git a/server/netty-server/sync/src/test/scala/sttp/tapir/server/netty/sync/NettySyncServerTest.scala b/server/netty-server/sync/src/test/scala/sttp/tapir/server/netty/sync/NettySyncServerTest.scala index d5aae0d4ad..19ded1bbe7 100644 --- a/server/netty-server/sync/src/test/scala/sttp/tapir/server/netty/sync/NettySyncServerTest.scala +++ b/server/netty-server/sync/src/test/scala/sttp/tapir/server/netty/sync/NettySyncServerTest.scala @@ -11,7 +11,6 @@ import org.scalatest.funsuite.AsyncFunSuite import org.scalatest.matchers.should.Matchers.* import org.slf4j.LoggerFactory import ox.* -import ox.channels.* import sttp.capabilities.WebSockets import sttp.capabilities.fs2.Fs2Streams import sttp.client3.* @@ -21,11 +20,12 @@ import sttp.tapir.* import sttp.tapir.server.ServerEndpoint import sttp.tapir.server.tests.* import sttp.tapir.tests.* -import sttp.ws.{WebSocket, WebSocketFrame} -import java.util.concurrent.{CompletableFuture, TimeUnit} import scala.concurrent.Future import scala.concurrent.duration.FiniteDuration +import ox.flow.Flow +import scala.annotation.nowarn +import sttp.tapir.server.netty.NettySyncRequestTimeoutTests class NettySyncServerTest extends AsyncFunSuite with BeforeAndAfterAll { @@ -43,46 +43,14 @@ class NettySyncServerTest extends AsyncFunSuite with BeforeAndAfterAll { .tests() ++ new ServerGracefulShutdownTests(createServerTest, sleeper).tests() ++ new ServerWebSocketTests(createServerTest, OxStreams, autoPing = true, failingPipe = true, handlePong = true) { - override def functionToPipe[A, B](f: A => B): OxStreams.Pipe[A, B] = ox ?=> in => in.map(f) - override def emptyPipe[A, B]: OxStreams.Pipe[A, B] = _ => Source.empty - - import createServerTest._ - override def tests(): List[Test] = super.tests() ++ List({ - val released: CompletableFuture[Boolean] = new CompletableFuture[Boolean]() - testServer( - endpoint.out(webSocketBody[String, CodecFormat.TextPlain, String, CodecFormat.TextPlain].apply(streams)), - "closes supervision scope when client closes Web Socket" - )((_: Unit) => - val pipe: OxStreams.Pipe[String, String] = in => { - releaseAfterScope { - released.complete(true).discard - } - in - } - Right(pipe) - ) { (backend, baseUri) => - basicRequest - .response(asWebSocket { (ws: WebSocket[IO]) => - for { - _ <- ws.sendText("test1") - _ <- ws.close() - _ <- ws.receiveText() - closeResponse <- ws.eitherClose(ws.receiveText()) - } yield closeResponse - }) - .get(baseUri.scheme("ws")) - .send(backend) - .map { r => - r.body.value shouldBe Left(WebSocketFrame.Close(1000, "normal closure")) - released.get(15, TimeUnit.SECONDS) shouldBe true - } - } - }) - }.tests() + override def functionToPipe[A, B](f: A => B): OxStreams.Pipe[A, B] = _.map(f) + override def emptyPipe[A, B]: OxStreams.Pipe[A, B] = _ => Flow.empty + }.tests() ++ + NettySyncRequestTimeoutTests(eventLoopGroup, backend).tests() tests.foreach { t => if (testNameFilter.forall(filter => t.name.contains(filter))) { - implicit val pos: Position = t.pos + @nowarn implicit val pos: Position = t.pos // used by test macro this.test(t.name)(t.f()) } diff --git a/server/netty-server/sync/src/test/scala/sttp/tapir/server/netty/sync/perf/NettySyncServerRunner.scala b/server/netty-server/sync/src/test/scala/sttp/tapir/server/netty/sync/perf/NettySyncServerRunner.scala index b020b85df1..b8b70c4939 100644 --- a/server/netty-server/sync/src/test/scala/sttp/tapir/server/netty/sync/perf/NettySyncServerRunner.scala +++ b/server/netty-server/sync/src/test/scala/sttp/tapir/server/netty/sync/perf/NettySyncServerRunner.scala @@ -2,6 +2,7 @@ package sttp.tapir.server.netty.sync.perf import ox.* import ox.channels.* +import ox.flow.Flow import sttp.shared.Identity import sttp.tapir.server.netty.sync.NettySyncServerOptions import sttp.tapir.server.netty.sync.NettySyncServerBinding @@ -57,12 +58,12 @@ object NettySyncServerRunner { val wsBaseEndpoint = endpoint.get.in("ws" / "ts") - val wsPipe: OxStreams.Pipe[Long, Long] = { in => - fork { - in.drain() - } - Source.tick(WebSocketSingleResponseLag).map(_ => System.currentTimeMillis()) - } + val wsPipe: OxStreams.Pipe[Long, Long] = in => + in.drain() + .merge( + Flow.tick(WebSocketSingleResponseLag).map(_ => System.currentTimeMillis()), + propagateDoneLeft = true + ) val wsEndpoint: Endpoint[Unit, Unit, Unit, OxStreams.Pipe[Long, Long], OxStreams with WebSockets] = wsBaseEndpoint .out( diff --git a/server/pekko-http-server/src/main/scala/sttp/tapir/server/pekkohttp/PekkoBodyListener.scala b/server/pekko-http-server/src/main/scala/sttp/tapir/server/pekkohttp/PekkoBodyListener.scala index 73b1606856..b5a9b78c4c 100644 --- a/server/pekko-http-server/src/main/scala/sttp/tapir/server/pekkohttp/PekkoBodyListener.scala +++ b/server/pekko-http-server/src/main/scala/sttp/tapir/server/pekkohttp/PekkoBodyListener.scala @@ -14,7 +14,7 @@ class PekkoBodyListener(implicit ec: ExecutionContext) extends BodyListener[Futu override def onComplete(body: PekkoResponseBody)(cb: Try[Unit] => Future[Unit]): Future[PekkoResponseBody] = { body match { case ws @ Left(_) => cb(Success(())).map(_ => ws) - case Right(e @ HttpEntity.Empty) => + case Right(e) if e.isKnownEmpty => Future.successful(Right(e)).andThen { case _ => cb(Success(())) } case Right(e: UniversalEntity) => Future.successful( diff --git a/server/pekko-http-server/src/test/scala/sttp/tapir/server/pekkohttp/PekkoHttpServerTest.scala b/server/pekko-http-server/src/test/scala/sttp/tapir/server/pekkohttp/PekkoHttpServerTest.scala index edf58cb4db..c3dfd25c28 100644 --- a/server/pekko-http-server/src/test/scala/sttp/tapir/server/pekkohttp/PekkoHttpServerTest.scala +++ b/server/pekko-http-server/src/test/scala/sttp/tapir/server/pekkohttp/PekkoHttpServerTest.scala @@ -14,14 +14,19 @@ import sttp.capabilities.pekko.PekkoStreams import sttp.client3._ import sttp.client3.pekkohttp.PekkoHttpBackend import sttp.model.sse.ServerSentEvent +import sttp.model.Header +import sttp.model.MediaType import sttp.monad.FutureMonad import sttp.monad.syntax._ import sttp.tapir._ import sttp.tapir.server.interceptor._ +import sttp.tapir.server.interceptor.metrics.MetricsRequestInterceptor +import sttp.tapir.server.metrics.{EndpointMetric, Metric} import sttp.tapir.server.tests._ import sttp.tapir.tests.{Test, TestSuite} import java.util.UUID +import java.util.concurrent.atomic.AtomicInteger import scala.concurrent.Future import scala.util.Random @@ -98,6 +103,58 @@ class PekkoHttpServerTest extends TestSuite with EitherValues { basicRequest.post(uri"http://localhost:$port").body("test123").send(backend).map(_.body shouldBe Right("replaced")) } .unsafeToFuture() + }, + Test("execute metrics interceptors for empty body and json content type") { + val e = endpoint.post.in(stringBody) + .out(stringBody) + .out(header(Header.contentType(MediaType.ApplicationJson))) + .serverLogicSuccess[Future](body => Future.successful(body)) + + class DummyMetric { + val onRequestCnt = new AtomicInteger(0) + val onEndpointRequestCnt = new AtomicInteger(0) + val onResponseHeadersCnt = new AtomicInteger(0) + val onResponseBodyCnt = new AtomicInteger(0) + } + val metric = new DummyMetric() + val customMetrics: Metric[Future, DummyMetric] = + Metric( + metric = metric, + onRequest = (_, metric, me) => + me.eval { + metric.onRequestCnt.incrementAndGet() + EndpointMetric( + onEndpointRequest = Some((_) => + me.eval(metric.onEndpointRequestCnt.incrementAndGet()), + ), + onResponseHeaders = Some((_, _) => + me.eval(metric.onResponseHeadersCnt.incrementAndGet()), + ), + onResponseBody = Some((_, _) => + me.eval(metric.onResponseBodyCnt.incrementAndGet()), + ), + onException = None, + ) + }, + ) + val route = PekkoHttpServerInterpreter( + PekkoHttpServerOptions.customiseInterceptors + .metricsInterceptor(new MetricsRequestInterceptor[Future](List(customMetrics), Seq.empty)) + .options + ).toRoute(e) + + interpreter + .server(NonEmptyList.of(route)) + .use { port => + basicRequest.post(uri"http://localhost:$port").body("").send(backend).map { response => + response.body shouldBe Right("") + metric.onRequestCnt.get() shouldBe 1 + metric.onEndpointRequestCnt.get() shouldBe 1 + metric.onResponseHeadersCnt.get() shouldBe 1 + metric.onResponseBodyCnt.get() shouldBe 1 + } + } + .unsafeToFuture() } ) def drainPekko(stream: PekkoStreams.BinaryStream): Future[Unit] = diff --git a/server/sttp-stub-server/src/main/scala/sttp/tapir/server/stub/SttpRequestBody.scala b/server/sttp-stub-server/src/main/scala/sttp/tapir/server/stub/SttpRequestBody.scala index c8adc730d1..6276e503dd 100644 --- a/server/sttp-stub-server/src/main/scala/sttp/tapir/server/stub/SttpRequestBody.scala +++ b/server/sttp-stub-server/src/main/scala/sttp/tapir/server/stub/SttpRequestBody.scala @@ -10,6 +10,12 @@ import sttp.tapir.server.interpreter.{RawValue, RequestBody} import java.io.{ByteArrayInputStream, ByteArrayOutputStream, InputStream} import java.nio.ByteBuffer import scala.annotation.tailrec +import sttp.client3 +import sttp.model.Part +import sttp.model.MediaType +import sttp.tapir.FileRange +import java.nio.file.Files +import java.io.FileInputStream class SttpRequestBody[F[_]](implicit ME: MonadError[F]) extends RequestBody[F, AnyStreams] { override val streams: AnyStreams = AnyStreams @@ -26,27 +32,32 @@ class SttpRequestBody[F[_]](implicit ME: MonadError[F]) extends RequestBody[F, A case RawBodyType.InputStreamRangeBody => ME.unit(RawValue(InputStreamRange(() => new ByteArrayInputStream(bytes)))) case _: RawBodyType.MultipartBody => ME.error(new UnsupportedOperationException) } - case _ => throw new IllegalArgumentException("Stream body provided while endpoint accepts raw body type") + case Right(parts) => + bodyType match { + case mp: RawBodyType.MultipartBody => ME.unit(RawValue(extractMultipartParts(parts, mp))) + case _ => throw new IllegalArgumentException(s"Multipart body provided while endpoint accepts raw body type: ${bodyType}") + } } - override def toStream(serverRequest: ServerRequest, maxBytes: Option[Long]): streams.BinaryStream = body(serverRequest) match { - case Right(stream) => stream - case _ => throw new IllegalArgumentException("Raw body provided while endpoint accepts stream body") - } + override def toStream(serverRequest: ServerRequest, maxBytes: Option[Long]): streams.BinaryStream = + sttpRequest(serverRequest).body match { + case StreamBody(s) => s + case _ => throw new IllegalArgumentException("Raw body provided while endpoint accepts stream body") + } private def sttpRequest(serverRequest: ServerRequest) = serverRequest.underlying.asInstanceOf[Request[_, _]] - /** Either bytes or any stream */ - private def body(serverRequest: ServerRequest): Either[Array[Byte], Any] = sttpRequest(serverRequest).body match { + private def body(serverRequest: ServerRequest): Either[Array[Byte], Seq[Part[client3.RequestBody[_]]]] = sttpRequest( + serverRequest + ).body match { case NoBody => Left(Array.emptyByteArray) case StringBody(s, encoding, _) => Left(s.getBytes(encoding)) case ByteArrayBody(b, _) => Left(b) case ByteBufferBody(b, _) => Left(b.array()) case InputStreamBody(b, _) => Left(toByteArray(b)) case FileBody(f, _) => Left(f.readAsByteArray) - case StreamBody(s) => Right(s) - case MultipartBody(_) => - throw new IllegalArgumentException("Stub cannot handle multipart bodies") + case StreamBody(_) => throw new IllegalArgumentException("Stream body provided while endpoint accepts raw body type") + case MultipartBody(parts) => Right(parts) } private def toByteArray(is: InputStream): Array[Byte] = { @@ -66,4 +77,56 @@ class SttpRequestBody[F[_]](implicit ME: MonadError[F]) extends RequestBody[F, A transfer() os.toByteArray } + + private def extractMultipartParts(parts: Seq[Part[client3.RequestBody[_]]], bodyType: RawBodyType.MultipartBody): List[Part[Any]] = { + parts.flatMap { part => + bodyType.partType(part.name).flatMap { partType => + val body = extractPartBody(part, partType) + Some( + Part( + name = part.name, + body = body, + contentType = part.contentType.flatMap(ct => MediaType.parse(ct).toOption), + fileName = part.fileName + ) + ) + } + }.toList + } + + private def extractPartBody[B](part: Part[client3.RequestBody[_]], bodyType: RawBodyType[B]): Any = { + part.body match { + case ByteArrayBody(b, _) => + bodyType match { + case RawBodyType.StringBody(_) => b + case RawBodyType.ByteArrayBody => b + case RawBodyType.ByteBufferBody => ByteBuffer.wrap(b) + case RawBodyType.InputStreamBody => new ByteArrayInputStream(b) + case RawBodyType.InputStreamRangeBody => InputStreamRange(() => new ByteArrayInputStream(b)) + case RawBodyType.FileBody => throw new IllegalArgumentException("ByteArray part provided while expecting a File part") + case _: RawBodyType.MultipartBody => throw new IllegalArgumentException("Nested multipart bodies are not allowed") + } + case FileBody(f, _) => + bodyType match { + case RawBodyType.FileBody => FileRange(f.toFile) + case RawBodyType.ByteArrayBody => Files.readAllBytes(f.toPath) + case RawBodyType.ByteBufferBody => ByteBuffer.wrap(Files.readAllBytes(f.toPath)) + case RawBodyType.InputStreamBody => new FileInputStream(f.toFile) + case _ => throw new IllegalArgumentException(s"File part provided, while expecting $bodyType") + } + case StringBody(s, charset, _) => + bodyType match { + case RawBodyType.StringBody(_) => s + case RawBodyType.ByteArrayBody => s.getBytes(charset) + case RawBodyType.ByteBufferBody => ByteBuffer.wrap(s.getBytes(charset)) + case _ => throw new IllegalArgumentException(s"String part provided, while expecting $bodyType") + } + case InputStreamBody(is, _) => + bodyType match { + case RawBodyType.InputStreamBody => is + case _ => throw new IllegalArgumentException(s"InputStream part provided, while expecting $bodyType") + } + case _ => throw new IllegalArgumentException(s"Unsupported part body type provided: ${part.body}") + } + } } diff --git a/server/sttp-stub-server/src/test/scala/sttp/tapir/server/stub/TapirStubInterpreterTest.scala b/server/sttp-stub-server/src/test/scala/sttp/tapir/server/stub/TapirStubInterpreterTest.scala index 3fbc0050fd..ec96417127 100644 --- a/server/sttp-stub-server/src/test/scala/sttp/tapir/server/stub/TapirStubInterpreterTest.scala +++ b/server/sttp-stub-server/src/test/scala/sttp/tapir/server/stub/TapirStubInterpreterTest.scala @@ -14,6 +14,12 @@ import sttp.tapir.server.interceptor.exception.ExceptionHandler import sttp.tapir.server.interceptor.reject.RejectHandler import sttp.tapir.server.interceptor.{CustomiseInterceptors, Interceptor} import sttp.tapir.server.model.ValuedEndpointOutput +import sttp.tapir.generic.auto._ +import sttp.tapir.tests.TestUtil.{readFromFile, writeToFile} +import sttp.model.Part +import sttp.tapir.TapirFile +import scala.concurrent.Await +import scala.concurrent.duration.DurationInt class TapirStubInterpreterTest extends AnyFlatSpec with Matchers { @@ -204,8 +210,136 @@ class TapirStubInterpreterTest extends AnyFlatSpec with Matchers { response.body shouldBe Left("Internal server error") response.code shouldBe StatusCode.InternalServerError } + + it should "handle multipart body" in { + // given + val e = + endpoint.post + .in("api" / "multipart") + .in(multipartBody) + .out(stringBody) + + val server = TapirStubInterpreter(SttpBackendStub(IdMonad)) + .whenEndpoint(e) + .thenRespond("success") + .backend() + + // when + val response = sttp.client3.basicRequest + .post(uri"http://test.com/api/multipart") + .multipartBody( + multipart("name", "abc"), + multipartFile("file", writeToFile("file_content")) + ) + .send(server) + + // then + response.body shouldBe Right("success") + } + + it should "correctly process a multipart body" in { + // given + val e = + endpoint.post + .in("api" / "multipart") + .in(multipartBody) + .out(stringBody) + + val server = TapirStubInterpreter(SttpBackendStub.synchronous) + .whenServerEndpointRunLogic(e.serverLogic((multipartData) => { + val partOpt = multipartData.find(_.name == "name") + val fileOpt = multipartData.find(_.name == "file") + + (partOpt, fileOpt) match { + case (Some(part), Some(filePart)) => + val partData = new String(part.body) + val fileData = new String(filePart.body) + IdMonad.unit(Right("name: " + partData + " file: " + fileData)) + + case (Some(_), None) => + IdMonad.unit(Right("File part not found")) + + case (None, Some(_)) => + IdMonad.unit(Right("Part not found")) + + case (None, None) => + IdMonad.unit(Right("Both parts not found")) + } + })) + .backend() + + // when + val response = sttp.client3.basicRequest + .post(uri"http://test.com/api/multipart") + .multipartBody( + multipart("name", "abc"), + multipartFile("file", writeToFile("file_content")) + ) + .send(server) + + // then + response.body shouldBe Right("name: abc file: file_content") + } + + it should "correctly handle derived multipart body" in { + // given + val e = + endpoint.post + .in("api" / "multipart") + .in(multipartBody[MultipartData]) + .out(stringBody) + + val server = TapirStubInterpreter(SttpBackendStub(IdMonad)) + .whenServerEndpointRunLogic(e.serverLogic(multipartData => { + val fileContent = Await.result(readFromFile(multipartData.file.body), 3.seconds) + IdMonad.unit(Right("name: " + multipartData.name + " year: " + multipartData.year + " file: " + fileContent)) + })) + .backend() + + // when + val response = sttp.client3.basicRequest + .post(uri"http://test.com/api/multipart") + .multipartBody( + multipart("name", "abc"), + multipart("year", "2024"), + multipartFile("file", writeToFile("file_content")) + ) + .send(server) + + // then + response.body shouldBe Right("name: abc year: 2024 file: file_content") + } + + it should "throw exception when bytearray body provided while endpoint accepts fileBody" in { + // given + val e = + endpoint.post + .in("api" / "multipart") + .in(multipartBody[MultipartData]) + .out(stringBody) + + val server = TapirStubInterpreter(SttpBackendStub(IdMonad)) + .whenEndpoint(e) + .thenRespond("success") + .backend() + + // when + val response = the[IllegalArgumentException] thrownBy sttp.client3.basicRequest + .post(uri"http://test.com/api/multipart") + .multipartBody( + multipart("name", "abc"), + multipart("year", "2024"), + multipart("file", "file_content".getBytes()) + ) + .send(server) + + // then + response.getMessage shouldBe "ByteArray part provided while expecting a File part" + } } +case class MultipartData(name: String, year: Int, file: Part[TapirFile]) + object ProductsApi { val getProduct: Endpoint[Unit, Unit, String, String, Any] = endpoint.get 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 544c8d4773..ec84e2d528 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 @@ -416,6 +416,38 @@ class ServerBasicTests[F[_], OPTIONS, ROUTE]( pureResult(s"fruit: $fruit".asRight[Unit]) ) { (backend, baseUri) => basicRequest.get(uri"$baseUri?fruit=orange").send(backend).map(_.contentLength shouldBe Some(13)) + }, + // #4194: large payloads where released too soon in the netty server interpreters, corrupting the json and resulting in parse errors + { + val largePayloadSize = 1024 * 1024 * 5 + testServer( + endpoint.post.in("api" / "echo").in(stringBody).out(stringBody).maxRequestBodyLength(largePayloadSize + 100), + "multiple large requests parsing & serialising JSON" + )((s: String) => pureResult(s.asRight[Unit])) { (backend, baseUri) => + val largePayload = Iterator.continually('A' to 'Z').flatten.take(largePayloadSize).mkString + (1 to 100).toList + .traverse { i => + basicRequest + .post(uri"$baseUri/api/echo") + .body(largePayload) + .send(backend) + .map { response => + if (response.code != StatusCode.Ok || response.body != Right(largePayload)) { + val detail = response.body match { + case Left(e) => fail(s"error response: $e") + case Right(b) if b.length != largePayload.length => s"body length: ${b.length}, expected: ${largePayload.length}" + case Right(b) => + val original = largePayload.getBytes() + val received = b.getBytes() + val firstDifference = original.zip(received).indexWhere { case (a, b) => a != b } + s"first difference on byte $firstDifference, expected: ${original(firstDifference)}, received: ${received(firstDifference)}" + } + fail(s"Failed on iteration $i, got response code: ${response.code}; $detail") + } + } + } + .map(_ => succeed) + } } ) @@ -461,6 +493,11 @@ class ServerBasicTests[F[_], OPTIONS, ROUTE]( (backend, baseUri) => basicRequest.get(uri"$baseUri/api").send(backend).map(_.code shouldBe StatusCode.Ok) }, + testServer(in_path_paths_out_header_body, "Encoded path should be decoded") { case (i, paths) => + pureResult(Right((i, paths.last))) + } { (backend, baseUri) => + basicRequest.get(uri"$baseUri/api/15/and/MIN%2FMAX").send(backend).map(_.body shouldBe Right("MIN/MAX")) + }, testServer(in_single_path, "single path should match single/ path")((_: Unit) => pureResult(Either.right[Unit, Unit](()))) { (backend, baseUri) => basicRequest.get(uri"$baseUri/api/").send(backend).map(_.code shouldBe StatusCode.Ok) diff --git a/serverless/aws/cdk/src/main/resources/app-template/package.json b/serverless/aws/cdk/src/main/resources/app-template/package.json index fab3667600..0336088658 100644 --- a/serverless/aws/cdk/src/main/resources/app-template/package.json +++ b/serverless/aws/cdk/src/main/resources/app-template/package.json @@ -21,7 +21,7 @@ "typescript": "~3.9.7" }, "dependencies": { - "aws-cdk-lib": "2.80.0", + "aws-cdk-lib": "2.162.1", "constructs": "^10.0.0" } }