diff --git a/.github/labeler.yml b/.github/labeler.yml index e703eeb11c..560a28fc68 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -6,9 +6,11 @@ labels: - "build.sbt" - "project/Versions.scala" - "project/plugins.sbt" + - "examples\\/src\\/main\\/scala\\/.*" - label: "dependency" authors: ["softwaremill-ci"] files: - "build.sbt" - "project/Versions.scala" - "project/plugins.sbt" + - "examples\\/src\\/main\\/scala\\/.*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c35d8ca65..1e3b48d6ea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,6 +68,11 @@ jobs: sudo apt-get update sudo apt-get install libidn2-dev libcurl3-dev echo "STTP_NATIVE=1" >> $GITHUB_ENV + - name: Install scala-cli + if: matrix.target-platform == 'JVM' && matrix.java == '21' && matrix.scala-version == '3' + uses: VirtusLab/scala-cli-setup@main + with: + jvm: '' # needed because scala-cli-setup otherwise forces the installation of their default JVM (17) - name: Enable Loom-specific modules if: matrix.java == '21' run: echo "ONLY_LOOM=1" >> $GITHUB_ENV @@ -76,6 +81,9 @@ jobs: - name: Compile documentation if: matrix.target-platform == 'JVM' && matrix.java == '21' run: sbt $SBT_JAVA_OPTS -v compileDocumentation + - name: Verify that examples compile using Scala CLI + if: matrix.target-platform == 'JVM' && matrix.java == '21' && matrix.scala-version == '3' + run: sbt $SBT_JAVA_OPTS -v "project examples3" verifyExamplesCompileUsingScalaCli - name: Test if: matrix.target-platform == 'JVM' && matrix.scala-version == '2.12' uses: nick-fields/retry@v2 @@ -208,15 +216,7 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 2 - # count number of files changed - - name: Count number of files changed - id: count-changed-files - run: | - N=$(git diff --name-only -r HEAD^1 HEAD | wc -w) - echo "changed_files_num=$N" >> $GITHUB_OUTPUT - name: Launch labeler - # skip if more than one file changed - if: steps.count-changed-files.outputs.changed_files_num == 1 uses: srvaroa/labeler@master env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" diff --git a/README.md b/README.md index d5a6c20885..c9fe5fab1e 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,7 @@ tapir documentation is available at [tapir.softwaremill.com](http://tapir.softwa Add the following dependency: ```sbt -"com.softwaremill.sttp.tapir" %% "tapir-core" % "1.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-core" % "1.10.13" ``` Then, import: diff --git a/build.sbt b/build.sbt index 07aaa1cb6a..0893e009ba 100644 --- a/build.sbt +++ b/build.sbt @@ -29,6 +29,7 @@ val ideScalaVersion = scala3 lazy val clientTestServerPort = settingKey[Int]("Port to run the client interpreter test server on") lazy val startClientTestServer = taskKey[Unit]("Start a http server used by client interpreter tests") lazy val generateMimeByExtensionDB = taskKey[Unit]("Generate the mime by extension DB") +lazy val verifyExamplesCompileUsingScalaCli = taskKey[Unit]("Verify that each example compiles using Scala CLI") concurrentRestrictions in Global ++= Seq( Tags.limit(Tags.Test, 1), @@ -65,6 +66,9 @@ val commonSettings = commonSmlBuildSettings ++ ossPublishSettings ++ Seq( val files1 = UpdateVersionInDocs(sLog.value, organization.value, version.value) Def.task { (documentation.jvm(documentationScalaVersion) / mdoc).toTask("").value + // Generating the list only after mdoc is done (as it overrides what's in generated_doc) + // For the root project the sourceDirectory points to src, so ../ will point to the root directory of the project + GenerateListOfExamples(sLog.value, sourceDirectory.value.getParentFile) files1 ++ Seq(file("generated-doc/out")) } }.value, @@ -920,8 +924,8 @@ lazy val jsoniterScala: ProjectMatrix = (projectMatrix in file("json/jsoniter")) .settings( name := "tapir-jsoniter-scala", libraryDependencies ++= Seq( - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.30.4", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.30.4" % Test, + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.30.7", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.30.7" % Test, scalaTest.value % Test ) ) @@ -2046,7 +2050,8 @@ lazy val examples: ProjectMatrix = (projectMatrix in file("examples")) logback ), publishArtifact := false, - Compile / run / fork := true + Compile / run / fork := true, + verifyExamplesCompileUsingScalaCli := VerifyExamplesCompileUsingScalaCli(sLog.value, sourceDirectory.value) ) .jvmPlatform(scalaVersions = List(examplesScalaVersion)) .dependsOn( diff --git a/doc/_static/css/custom.css b/doc/_static/css/custom.css new file mode 100644 index 0000000000..46ec85acca --- /dev/null +++ b/doc/_static/css/custom.css @@ -0,0 +1,42 @@ +/* general style for all example tags */ +.example-tag { + border-width: 1px; + border-radius: 9999px; + border-style: solid; + padding-left: 0.5rem; + padding-right: 0.5rem; + margin-right: 0.25rem; + margin-top: 0.25rem; + margin-bottom: 0.25rem; +} + +/* different colors for specific tags */ +.example-effects { + color: rgb(193 21 116); + background-color: rgb(253 242 250); + border-color: rgb(252 206 238); +} + +.example-json { + color: rgb(185 56 21); + background-color: rgb(254 246 238); + border-color: rgb(249 219 175); +} + +.example-server { + color: rgb(6 118 71); + background-color: rgb(236 253 243); + border-color: rgb(169 239 197); +} + +.example-docs { + color: rgb(52 64 84); + background-color: rgb(249 250 251); + border-color: rgb(234 236 240); +} + +.example-client { + color: rgb(6 89 134); + background-color: rgb(240 249 255); + border-color: rgb(185 230 254); +} diff --git a/doc/adopters.md b/doc/adopters.md new file mode 100644 index 0000000000..6b86241a9a --- /dev/null +++ b/doc/adopters.md @@ -0,0 +1,43 @@ +# Adopters + +Is your company already using tapir? We're continually expanding the "adopters" section in the documentation; the more the merrier! It would be great to feature your company's logo, but in order to do that, we'll need written permission to avoid any legal misunderstandings. + +Please email us at [tapir@softwaremill.com](mailto:tapir@softwaremill.com) from your company's email with a link to your logo (if we can use it, of course!) or with details who to kindly ask for permission to feature the logo in tapir's documentation. We'll handle the rest. + +Thank you! + +
+Adobe +Swisscom +Swissborg +
+
+Kaizo +Process Street +Tranzzo +
+
+Kelkoo group +SoftwareMill +Carvana +
+
+Moneyfarm +Ocado +Wegtam +
+
+Broad +Kensu +Colisweb +
+
+iceo +dpg +hunters +
+
+moia +pits +hootsuite +
diff --git a/doc/conf.py b/doc/conf.py index 5afa049774..d7df20c3ab 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -101,6 +101,12 @@ # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] +# These paths are either relative to html_static_path +# or fully qualified paths (eg. https://...) +html_css_files = [ + 'css/custom.css', +] + # Custom sidebar templates, must be a dictionary that maps document names # to template names. # diff --git a/doc/endpoint/customtypes.md b/doc/endpoint/customtypes.md index 5e4a71ce11..316b678dbd 100644 --- a/doc/endpoint/customtypes.md +++ b/doc/endpoint/customtypes.md @@ -1,4 +1,4 @@ -# Custom types +# Adding support for custom types To support a custom type, you'll need to provide an implicit `Codec` for that type, or the components to create such a codec. diff --git a/doc/endpoint/enumerations.md b/doc/endpoint/enumerations.md index f07f8c6095..574d5a7fa9 100644 --- a/doc/endpoint/enumerations.md +++ b/doc/endpoint/enumerations.md @@ -274,7 +274,7 @@ case class Foo(aOrB: "a" | "b", optA: Option["a"]) derives Schema ### Creating an enum schema by hand -Creating an enumeration [schema](schema.md) by hand is exactly the same as for any other type. The only difference +Creating an enumeration [schema](schemas.md) by hand is exactly the same as for any other type. The only difference is that an enumeration [validator](validation.md) has to be added to the schema. ## Next diff --git a/doc/endpoint/integrations.md b/doc/endpoint/integrations.md index df96382423..96e3db7d54 100644 --- a/doc/endpoint/integrations.md +++ b/doc/endpoint/integrations.md @@ -1,4 +1,4 @@ -# Datatypes integrations +# Third-party datatype libraries integrations ```{note} Note that the codecs defined by the tapir integrations are used only when the specific types (e.g. enumerations) are diff --git a/doc/endpoint/json.md b/doc/endpoint/json.md index 81116b6638..95f5fc6edc 100644 --- a/doc/endpoint/json.md +++ b/doc/endpoint/json.md @@ -53,7 +53,7 @@ To use [Circe](https://github.com/circe/circe), add the following dependency to "com.softwaremill.sttp.tapir" %% "tapir-json-circe" % "@VERSION@" ``` -Next, import the package (or extend the `TapirJsonCirce` trait, see [MyTapir](../mytapir.md)): +Next, import the package (or extend the `TapirJsonCirce` trait, see [MyTapir](../other/mytapir.md)): ```scala mdoc:compile-only import sttp.tapir.json.circe.* @@ -125,7 +125,7 @@ To use [µPickle](http://www.lihaoyi.com/upickle/) add the following dependency "com.softwaremill.sttp.tapir" %% "tapir-json-upickle" % "@VERSION@" ``` -Next, import the package (or extend the `TapirJsonuPickle` trait, see [MyTapir](../mytapir.md) and add `TapirJsonuPickle` not `TapirCirceJson`): +Next, import the package (or extend the `TapirJsonuPickle` trait, see [MyTapir](../other/mytapir.md) and add `TapirJsonuPickle` not `TapirCirceJson`): ```scala mdoc:compile-only import sttp.tapir.json.upickle.* @@ -165,7 +165,7 @@ For **Play 2.9** use: "com.softwaremill.sttp.tapir" %% "tapir-json-play29" % "@VERSION@" ``` -Next, import the package (or extend the `TapirJsonPlay` trait, see [MyTapir](../mytapir.md) and add `TapirJsonPlay` not `TapirCirceJson`): +Next, import the package (or extend the `TapirJsonPlay` trait, see [MyTapir](../other/mytapir.md) and add `TapirJsonPlay` not `TapirCirceJson`): ```scala mdoc:compile-only import sttp.tapir.json.play.* @@ -181,7 +181,7 @@ To use [Spray JSON](https://github.com/spray/spray-json) add the following depen "com.softwaremill.sttp.tapir" %% "tapir-json-spray" % "@VERSION@" ``` -Next, import the package (or extend the `TapirJsonSpray` trait, see [MyTapir](../mytapir.md) and add `TapirJsonSpray` not `TapirCirceJson`): +Next, import the package (or extend the `TapirJsonSpray` trait, see [MyTapir](../other/mytapir.md) and add `TapirJsonSpray` not `TapirCirceJson`): ```scala mdoc:compile-only import sttp.tapir.json.spray.* @@ -197,7 +197,7 @@ To use [Tethys JSON](https://github.com/tethys-json/tethys) add the following de "com.softwaremill.sttp.tapir" %% "tapir-json-tethys" % "@VERSION@" ``` -Next, import the package (or extend the `TapirJsonTethys` trait, see [MyTapir](../mytapir.md) and add `TapirJsonTethys` not `TapirCirceJson`): +Next, import the package (or extend the `TapirJsonTethys` trait, see [MyTapir](../other/mytapir.md) and add `TapirJsonTethys` not `TapirCirceJson`): ```scala mdoc:compile-only import sttp.tapir.json.tethysjson.* @@ -213,7 +213,7 @@ To use [Jsoniter-scala](https://github.com/plokhotnyuk/jsoniter-scala) add the f "com.softwaremill.sttp.tapir" %% "tapir-jsoniter-scala" % "@VERSION@" ``` -Next, import the package (or extend the `TapirJsonJsoniter` trait, see [MyTapir](../mytapir.md) and add `TapirJsonJsoniter` not `TapirCirceJson`): +Next, import the package (or extend the `TapirJsonJsoniter` trait, see [MyTapir](../other/mytapir.md) and add `TapirJsonJsoniter` not `TapirCirceJson`): ```scala mdoc:compile-only import sttp.tapir.json.jsoniter.* @@ -237,7 +237,7 @@ And one of the implementations: "org.json4s" %% "json4s-jackson" % "@JSON4S_VERSION@" ``` -Next, import the package (or extend the `TapirJson4s` trait, see [MyTapir](../mytapir.md) and add `TapirJson4s` instead of `TapirCirceJson`): +Next, import the package (or extend the `TapirJson4s` trait, see [MyTapir](../other/mytapir.md) and add `TapirJson4s` instead of `TapirCirceJson`): ```scala mdoc:compile-only import sttp.tapir.json.json4s.* @@ -259,7 +259,7 @@ To use [zio-json](https://github.com/zio/zio-json), add the following dependency ```scala "com.softwaremill.sttp.tapir" %% "tapir-json-zio" % "@VERSION@" ``` -Next, import the package (or extend the `TapirJsonZio` trait, see [MyTapir](../mytapir.md) and add `TapirJsonZio` instead of `TapirCirceJson`): +Next, import the package (or extend the `TapirJsonZio` trait, see [MyTapir](../other/mytapir.md) and add `TapirJsonZio` instead of `TapirCirceJson`): ```scala mdoc:compile-only import sttp.tapir.json.zio.* diff --git a/doc/endpoint/schemas.md b/doc/endpoint/schemas.md index b2d1a68872..38d998a780 100644 --- a/doc/endpoint/schemas.md +++ b/doc/endpoint/schemas.md @@ -119,7 +119,7 @@ the union type, as it's not possible to generate a runtime check for the generic ### Derivation for string-based constant union types e.g. `type AorB = "a" | "b"` -See [enumerations](enumerations.md#scala-3-string-based-constant-union-types-to-enum) on how to use string-based unions of constant types as enums. +See [enumerations](enumerations.html#scala-3-string-based-constant-union-types-to-enum) on how to use string-based unions of constant types as enums. ## Configuring derivation diff --git a/doc/endpoint/security.md b/doc/endpoint/security.md index 0fd010c1ae..1ccecfa50c 100644 --- a/doc/endpoint/security.md +++ b/doc/endpoint/security.md @@ -31,7 +31,7 @@ base64-encoded username/password combination, use: `basic[UsernamePassword]`. as a string, use: `bearer[String]`. * `auth.oauth2.authorizationCode(authorizationUrl, scopes, tokenUrl, refreshUrl): EndpointInput[String]`: creates an OAuth2 authorization using authorization code - sign in using an auth service (for documentation, requires defining also -the `oauth2-redirect.html`, see [Generating OpenAPI documentation](..docs/openapi.md)) +the `oauth2-redirect.html`, see [Generating OpenAPI documentation](../docs/openapi.md)) ## Authentication challenges diff --git a/doc/examples.md b/doc/examples.md index 704d70b0e2..be8151896a 100644 --- a/doc/examples.md +++ b/doc/examples.md @@ -1,35 +1,18 @@ -# Examples +# Examples by category -The [examples](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples) sub-project contains a number of runnable tapir usage examples, using various interpreters and showcasing different features. +The Tapir repository contains a number of how-to guides. If you're missing an example for your use-case, please let us +know by [reporting an issue](https://github.com/softwaremill/tapir)! -## Generate a tapir project +Each example is fully self-contained and can be run using [scala-cli](https://scala-cli.virtuslab.org) (you just need +to copy the content of the file, apart from scala-cli, no additional setup is required!). Hopefully this will make +experimenting with Tapir as frictionless as possible! -You can generate a simple tapir-based project using chosen features, build tool and effect system using [adopt-tapir](https://adopt-tapir.softwaremill.com). +Examples are tagged with the stack being used (Direct-style, cats-effect, ZIO, Future), server implementation, generated +documentation format etc. -Alternatively, you can generate a stub of a tapir-based application directly from the command line with `sbt new softwaremill/tapir.g8`. +```{eval-rst} +.. include:: includes/examples_list.md + :parser: markdown +``` -## Third-party examples -* http4s interpreter: [todo-backend](https://github.com/lolgab/snunit-tapir-example) -* quickstart using http4s: [a gitter8 template](https://codeberg.org/wegtam/http4s-tapir.g8). A new project can be created using: `sbt new https://codeberg.org/wegtam/http4s-tapir.g8.git` -* Scala Native application, [using Nginx Unit](https://github.com/lolgab/snunit-tapir-example). - -## Blogs, articles - -* [Migrating from Akka HTTP to tapir](https://softwaremill.com/migrating-from-akka-http-to-tapir/) -* [Benchmarking Tapir: Part 1](https://softwaremill.com/benchmarking-tapir-part-1/) -* [Benchmarking Tapir: Part 2](https://softwaremill.com/benchmarking-tapir-part-2/) -* [Tapir 1.0 released](https://softwaremill.com/tapir-1-0-released/) -* [Security improvements in tapir 0.19](https://softwaremill.com/security-improvements-in-tapir-0-19/) -* [Tapir serverless: a proof of concept](https://blog.softwaremill.com/tapir-serverless-a-proof-of-concept-6b8c9de4d396) -* [Designing tapir's WebSockets support](https://blog.softwaremill.com/designing-tapirs-websockets-support-ff1573166368) -* [Three easy endpoints](https://blog.softwaremill.com/three-easy-endpoints-a6cbd52b0a6e) -* [tAPIr's Endpoint meets ZIO's IO](https://blog.softwaremill.com/tapirs-endpoint-meets-zio-s-io-3278099c5e10) -* [Describe, then interpret: HTTP endpoints using tapir](https://blog.softwaremill.com/describe-then-interpret-http-endpoints-using-tapir-ac139ba565b0) -* [Functional pancakes](https://blog.softwaremill.com/functional-pancakes-cf70023f0dcb) - -## Videos - -* [ScalaLove 2020: Your HTTP endpoints are data, as well!](https://www.youtube.com/watch?v=yuQNgZgSFIc&t=944s) -* [Scalar 2020: A Functional Scala Stack For 2020](https://www.youtube.com/watch?v=DGlkap5kzGU) -* [ScalaWorld 2019: Designing Programmer-Friendly APIs](https://www.youtube.com/watch?v=I3loMuHnYqw) diff --git a/doc/external.md b/doc/external.md new file mode 100644 index 0000000000..5a9a07188f --- /dev/null +++ b/doc/external.md @@ -0,0 +1,33 @@ +# Articles, videos, other examples + +## Generate a tapir project + +You can generate a simple tapir-based project using chosen features, build tool and effect system using [adopt-tapir](https://adopt-tapir.softwaremill.com). + +Alternatively, you can generate a stub of a tapir-based application directly from the command line with `sbt new softwaremill/tapir.g8`. + +## Third-party examples + +* http4s interpreter: [todo-backend](https://github.com/lolgab/snunit-tapir-example) +* quickstart using http4s: [a gitter8 template](https://codeberg.org/wegtam/http4s-tapir.g8). A new project can be created using: `sbt new https://codeberg.org/wegtam/http4s-tapir.g8.git` +* Scala Native application, [using Nginx Unit](https://github.com/lolgab/snunit-tapir-example). + +## Blogs, articles + +* [Migrating from Akka HTTP to tapir](https://softwaremill.com/migrating-from-akka-http-to-tapir/) +* [Benchmarking Tapir: Part 1](https://softwaremill.com/benchmarking-tapir-part-1/) +* [Benchmarking Tapir: Part 2](https://softwaremill.com/benchmarking-tapir-part-2/) +* [Tapir 1.0 released](https://softwaremill.com/tapir-1-0-released/) +* [Security improvements in tapir 0.19](https://softwaremill.com/security-improvements-in-tapir-0-19/) +* [Tapir serverless: a proof of concept](https://blog.softwaremill.com/tapir-serverless-a-proof-of-concept-6b8c9de4d396) +* [Designing tapir's WebSockets support](https://blog.softwaremill.com/designing-tapirs-websockets-support-ff1573166368) +* [Three easy endpoints](https://blog.softwaremill.com/three-easy-endpoints-a6cbd52b0a6e) +* [tAPIr's Endpoint meets ZIO's IO](https://blog.softwaremill.com/tapirs-endpoint-meets-zio-s-io-3278099c5e10) +* [Describe, then interpret: HTTP endpoints using tapir](https://blog.softwaremill.com/describe-then-interpret-http-endpoints-using-tapir-ac139ba565b0) +* [Functional pancakes](https://blog.softwaremill.com/functional-pancakes-cf70023f0dcb) + +## Videos + +* [ScalaLove 2020: Your HTTP endpoints are data, as well!](https://www.youtube.com/watch?v=yuQNgZgSFIc&t=944s) +* [Scalar 2020: A Functional Scala Stack For 2020](https://www.youtube.com/watch?v=DGlkap5kzGU) +* [ScalaWorld 2019: Designing Programmer-Friendly APIs](https://www.youtube.com/watch?v=I3loMuHnYqw) diff --git a/doc/generate.md b/doc/generate.md new file mode 100644 index 0000000000..29302de31a --- /dev/null +++ b/doc/generate.md @@ -0,0 +1,21 @@ +# Generate a Tapir project + +Not sure how to start? + +We recommend the defaults below (Direct-style stack & Netty server); this requires Java 21+. Otherwise, +you might also try the Future stack + Netty, which works on Java 11+. + +If you'd like to include a JSON endpoint, using the [jsoniter](https://github.com/plokhotnyuk/jsoniter-scala) library +might a good choice! + +```{eval-rst} +.. raw:: html + + +``` diff --git a/doc/generator/sbt-openapi-codegen.md b/doc/generator/sbt-openapi-codegen.md index 96365c94aa..78c29630c0 100644 --- a/doc/generator/sbt-openapi-codegen.md +++ b/doc/generator/sbt-openapi-codegen.md @@ -89,8 +89,9 @@ If `openapiUseHeadTagForObjectName = true`, then the `GET /foo` and `GET /bar` `Baz.scala` file, containing a single `object Baz` with those endpoint definitions; the `PUT /foo` endpoint, by dint of having no tags, would be output to the `TapirGeneratedEndpoints` file, along with any schema and parameter definitions. -Files can be generated from multiple openapi schemas if `openapiAdditionalPackages` is configured; for example -```sbt +Files can be generated from multiple openapi schemas if `openapiAdditionalPackages` is configured; for example: + +```scala openapiAdditionalPackages := List( "sttp.tapir.generated.v1" -> baseDirectory.value / "src" / "main" / "resources" / "openapi_v1.yml") ``` diff --git a/doc/index.md b/doc/index.md index a46fe5fe86..9fe3f0bbe4 100644 --- a/doc/index.md +++ b/doc/index.md @@ -7,36 +7,40 @@ ## Intro -With tapir, you can describe HTTP API endpoints as immutable Scala values. Each endpoint can contain a number of -input and output parameters. An endpoint specification can be interpreted as: - -* a server, given the "business logic": a function, which computes output parameters based on input parameters. - Currently supported: - * [Akka HTTP](server/akkahttp.md) `Route`s/`Directive`s - * [Http4s](server/http4s.md) `HttpRoutes[F]` (using cats-effect or [ZIO](server/zio-http4s.md)) - * [Netty](server/netty.md) (using `Future`s, cats-effect or ZIO) - * [Helidon Níma](server/nima.md) (using JVM 21 Virtual Threads and direct style) - * [Finatra](server/finatra.md) `http.Controller` - * [Pekko HTTP](server/pekkohttp.md) `Route`s/`Directive`s - * [Play](server/play.md) `Route` - * [Vert.X](server/vertx.md) `Router => Route` (using `Future`s, cats-effect or ZIO) - * [ZIO Http](server/ziohttp.md) `Http` - * [Armeria](server/armeria.md) `HttpServiceWithRoutes` (using `Future`s, cats-effect or ZIO) - * [JDK HTTP](server/jdkhttp.md) `HttpHandler` (simple, synchronous API only) - * [aws](server/aws.md) through Lambda/SAM/Terraform - * [gRPC](grpc.md) -* a client, which is a function from input parameters to output parameters. - Currently supported: - * [sttp](client/sttp.md) - * [Play](client/play.md) - * [Http4s](client/http4s.md) -* documentation. Currently supported: - * [OpenAPI](docs/openapi.md) - * [AsyncAPI](docs/asyncapi.md) - * [Json Schema](docs/json-schema.md) - -Depending on how you prefer to explore the library, take a look at one of the [examples](examples.md) or read on -for a more detailed description of how tapir works! ScalaDocs are available at [javadoc.io](https://www.javadoc.io/doc/com.softwaremill.sttp.tapir). +Tapir is a library to describe HTTP APIs, expose them as a server, consume as a client, and automatically document +using open standards. + +Tapir is fast and developer-friendly. The endpoint definition APIs are crafted with readability and discoverability in +mind. Our Netty-based server is one of the best-performing Scala HTTP servers available. + +```scala +endpoint + .get.in("hello").in(query[String]("name")) + .out(stringBody) + .handleSuccess(name => s"Hello, $name!") +``` + +Tapir integrates with all major Scala stacks, so you can use your favorite approach to Functional Programming, while +leveraging all the benefits that Tapir brings! + +Seamless integration with the Scala and HTTP ecosystems is one of Tapir's major strengths: + +* all popular Scala HTTP server implementations are supported. You can define your entire API using Tapir, or expose +Tapir-managed routes alongside "native" ones. This is especially useful when gradually adopting Tapir, or using it for +selected use-cases. +* the Scala ecosystem is rich with libraries leveraging its type-safety and enhancing the developer's toolbox, +that's why Tapir provides integrations with many of such custom type, JSON and observability libraries +* documentation can be generated in the [OpenAPI](docs/openapi.md), [AsyncAPI](docs/asyncapi.md) and [JSON Schema](docs/json-schema.md) formats + +Depending on how you'd prefer to explore Tapir, this documentation has three main sections: + +1. There's a number of [tutorials](tutorials/01_hello_world.md), which provide a gentle introduction to the library +2. Nothing compares to tinkering with working code, that's why we've prepared [runnable examples](examples.md), + covering solutions to many "everyday" problems +3. Finally, the reference documentation describes all of Tapir's aspects in depth - take a look at the menu on + the left, starting with the "Endpoints" section + +ScalaDocs are available at [javadoc.io](https://www.javadoc.io/doc/com.softwaremill.sttp.tapir). Tapir is licensed under Apache2, the source code is [available on GitHub](https://github.com/softwaremill/tapir). @@ -49,64 +53,6 @@ Tapir is licensed under Apache2, the source code is [available on GitHub](https: * **abstraction**: re-use common endpoint definitions, as well as individual inputs/outputs * **library, not a framework**: integrates with your stack -## Adopt a tapir - -```{eval-rst} -.. raw:: html - - -``` - -## Adopters - -Is your company already using tapir? We're continually expanding the "adopters" section in the documentation; the more the merrier! It would be great to feature your company's logo, but in order to do that, we'll need written permission to avoid any legal misunderstandings. - -Please email us at [tapir@softwaremill.com](mailto:tapir@softwaremill.com) from your company's email with a link to your logo (if we can use it, of course!) or with details who to kindly ask for permission to feature the logo in tapir's documentation. We'll handle the rest. - -Thank you! - -
-Adobe -Swisscom -Swissborg -
-
-Kaizo -Process Street -Tranzzo -
-
-Kelkoo group -SoftwareMill -Carvana -
-
-Moneyfarm -Ocado -Wegtam -
-
-Broad -Kensu -Colisweb -
-
-iceo -dpg -hunters -
-
-moia -pits -hootsuite -
- ## Code teaser ```scala mdoc:compile-only @@ -179,16 +125,6 @@ sttp is a family of Scala HTTP-related projects, and currently includes: * [sttp shared](https://github.com/softwaremill/sttp-shared): shared web socket, FP abstractions, capabilities and streaming code. * [sttp apispec](https://github.com/softwaremill/sttp-apispec): OpenAPI, AsyncAPI and JSON Schema models. -## Sponsors - -Development and maintenance of sttp tapir is sponsored by [SoftwareMill](https://softwaremill.com), a software development and consulting company. We help clients scale their business through software. Our areas of expertise include backends, distributed systems, blockchain, machine learning and data analytics. - -[![](https://files.softwaremill.com/logo/logo.png "SoftwareMill")](https://softwaremill.com) - -## Commercial Support - -We offer commercial support for sttp and related technologies, as well as development services. [Contact us](https://softwaremill.com/contact/) to learn more about our offer! - ## Table of contents ```{eval-rst} @@ -197,8 +133,9 @@ We offer commercial support for sttp and related technologies, as well as develo :caption: Getting started quickstart - examples - stability + generate + adopters + support scala_2_3_platforms .. toctree:: @@ -211,6 +148,14 @@ We offer commercial support for sttp and related technologies, as well as develo tutorials/04_errors tutorials/05_multiple_inputs_outputs tutorials/06_error_variants + tutorials/07_cats_effect + +.. toctree:: + :maxdepth: 2 + :caption: How-to's + + examples + external .. toctree:: :maxdepth: 2 @@ -239,6 +184,7 @@ We offer commercial support for sttp and related technologies, as well as develo :maxdepth: 2 :caption: Server interpreters + server/overview server/akkahttp server/http4s server/zio-http4s @@ -292,11 +238,12 @@ We offer commercial support for sttp and related technologies, as well as develo :maxdepth: 2 :caption: Other subjects - other_interpreters - mytapir - grpc - troubleshooting - migrating - goals - contributing + other/stability + other/other_interpreters + other/mytapir + other/grpc + other/troubleshooting + other/migrating + other/goals + other/contributing diff --git a/doc/contributing.md b/doc/other/contributing.md similarity index 100% rename from doc/contributing.md rename to doc/other/contributing.md diff --git a/doc/goals.md b/doc/other/goals.md similarity index 100% rename from doc/goals.md rename to doc/other/goals.md diff --git a/doc/grpc.md b/doc/other/grpc.md similarity index 100% rename from doc/grpc.md rename to doc/other/grpc.md diff --git a/doc/migrating.md b/doc/other/migrating.md similarity index 98% rename from doc/migrating.md rename to doc/other/migrating.md index bbd915f12a..b0904b538f 100644 --- a/doc/migrating.md +++ b/doc/other/migrating.md @@ -14,11 +14,11 @@ - If your code sets `badRequestOnPathErrorIfPathShapeMatches = true` to override the default `false`, you can just remove this in tapir 1.5, it is the new default. - Similarly, if your code sets `.badRequestOnDecodeFailure` on endpoint path input, just remove this attribute. - If your code doesn't change this parameter and you update tapir, you should expect shape-matched path decoding failures to always become 400s, without attempting the next endpoint unless explicitly specified. -- If you want to override this behavior and force trying the next endpoint, add `.onDecodeFailureNextEndpoint` to the input where you expect such handling. See [error handling page](server/errors.md) for details. +- If you want to override this behavior and force trying the next endpoint, add `.onDecodeFailureNextEndpoint` to the input where you expect such handling. See [error handling page](../server/errors.md) for details. ## From 1.2 to 1.3 -- Static content endpoints from `sttp.tapir.static._` are deprecated in favor of the new `tapir-files` module. New methods are in `sttp.tapir.files._`: `staticFilesGetServerEndpoint`, `staticFilesHeadServerEndpoint`, `staticFilesServerEndpoints`, `staticResourcesGetServerEndpoint`, `staticResourcesHeadServerEndpoint`, `staticResourcesServerEndpoints`, etc. See the [updated documentation](endpoint/static.md). +- Static content endpoints from `sttp.tapir.static._` are deprecated in favor of the new `tapir-files` module. New methods are in `sttp.tapir.files._`: `staticFilesGetServerEndpoint`, `staticFilesHeadServerEndpoint`, `staticFilesServerEndpoints`, `staticResourcesGetServerEndpoint`, `staticResourcesHeadServerEndpoint`, `staticResourcesServerEndpoints`, etc. See the [updated documentation](../endpoint/static.md). - Respectively, use `sttp.tapir.files.FilesOptions` instead of `sttp.tapir.static.FilesOptions` - the `cats` integration module has been split into `cats` and `cats-effect`, with the latter containing the `CatsMonadError` class, providing a bridge between the sttp-internal `MonadError` and the cats-effect `Sync` typeclass. If you've been using this directly, you might need to update your dependencies. diff --git a/doc/mytapir.md b/doc/other/mytapir.md similarity index 100% rename from doc/mytapir.md rename to doc/other/mytapir.md diff --git a/doc/other_interpreters.md b/doc/other/other_interpreters.md similarity index 100% rename from doc/other_interpreters.md rename to doc/other/other_interpreters.md diff --git a/doc/stability.md b/doc/other/stability.md similarity index 100% rename from doc/stability.md rename to doc/other/stability.md diff --git a/doc/troubleshooting.md b/doc/other/troubleshooting.md similarity index 100% rename from doc/troubleshooting.md rename to doc/other/troubleshooting.md diff --git a/doc/quickstart.md b/doc/quickstart.md index 3fd1414079..93427ea085 100644 --- a/doc/quickstart.md +++ b/doc/quickstart.md @@ -24,12 +24,3 @@ endpoint. and see where auto-complete gets you! -## Scala 2.12 - -Partial unification is now enabled by default from Scala 2.13. However, if you're using Scala 2.12 or older, and don't -have it already, you'll want to to enable partial unification in the compiler (alternatively, you'll need to manually -provide type arguments in some cases). In sbt, this is: - -```scala -scalacOptions += "-Ypartial-unification" -``` diff --git a/doc/scala_2_3_platforms.md b/doc/scala_2_3_platforms.md index 5534455db6..db64bdab16 100644 --- a/doc/scala_2_3_platforms.md +++ b/doc/scala_2_3_platforms.md @@ -1,10 +1,10 @@ -# Scala 2, Scala 3 & platforms +# Scala 2, Scala 3; JVM, JS & Native Tapir is available for Scala 3.3+, Scala 2.13 and Scala 2.12, on the JVM, JS and Native platforms. -Note that some modules are unavailable for specific combinations of the above, specifically for Scala.JS and Scala -Native. The JVM modules require Java 11+, with a couple of exceptions, which require Java 21+ - this is marked in -the documentation. +Note that not all modules are available for all combinations of the above. This specifically applies to Scala.JS and +Scala Native, where support is limited. The JVM modules require Java 11+, with a couple of exceptions, which require +Java 21+ - this is marked in the documentation. ## In the documentation & examples @@ -51,3 +51,13 @@ class MyClass { } } ``` + +## Scala 2.12 + +Partial unification is now enabled by default from Scala 2.13. However, if you're using Scala 2.12 or older, and don't +have it already, you'll want to to enable partial unification in the compiler (alternatively, you'll need to manually +provide type arguments in some cases). In sbt, this is: + +```scala +scalacOptions += "-Ypartial-unification" +``` diff --git a/doc/server/aws.md b/doc/server/aws.md index aaeea4a751..4b3444c7d0 100644 --- a/doc/server/aws.md +++ b/doc/server/aws.md @@ -29,7 +29,7 @@ These are corresponding classes for each of the supported runtime: To start using any of the above add the following dependency: -```sbt +```scala "com.softwaremill.sttp.tapir" %% "tapir-aws-lambda" % "@VERSION@" ``` @@ -40,7 +40,7 @@ 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: -```sbt +```scala "com.softwaremill.sttp.tapir" %% "tapir-aws-sam" % "@VERSION@" "com.softwaremill.sttp.tapir" %% "tapir-aws-terraform" % "@VERSION@" "com.softwaremill.sttp.tapir" %% "tapir-aws-cdk" % "@VERSION@" diff --git a/doc/server/overview.md b/doc/server/overview.md new file mode 100644 index 0000000000..50af6aac36 --- /dev/null +++ b/doc/server/overview.md @@ -0,0 +1,23 @@ +# Overview of server integrations + +Server interpreters require the endpoint descriptions to be combined with "business logic": functions, which compute +an endpoint's output parameters based on input parameters. + +Tapir integrates with a number of HTTP server implementations, through **server interpreters**. We recommend starting +with the Netty-based server. However, if you already have experience with another server, or are using one in your +project already, just continue doing so, and enjoy seamless Tapir integration! + +Currently supported: +* [Netty](netty.md) (using direct-style, `Future`s, cats-effect or ZIO) +* [Http4s](http4s.md) `HttpRoutes[F]` (using cats-effect or [ZIO](zio-http4s.md)) +* [Pekko HTTP](pekkohttp.md) `Route`s/`Directive`s +* [ZIO Http](ziohttp.md) `Http` +* [Play](play.md) `Route` +* [Akka HTTP](akkahttp.md) `Route`s/`Directive`s +* [Helidon Níma](nima.md) (using JVM 21 Virtual Threads and direct style) +* [Finatra](finatra.md) `http.Controller` +* [Vert.X](vertx.md) `Router => Route` (using `Future`s, cats-effect or ZIO) +* [Armeria](armeria.md) `HttpServiceWithRoutes` (using `Future`s, cats-effect or ZIO) +* [JDK HTTP](jdkhttp.md) `HttpHandler` (simple, synchronous API only) +* [aws](aws.md) through Lambda/SAM/Terraform +* [gRPC](../other/grpc.md) diff --git a/doc/server/zio-http4s.md b/doc/server/zio-http4s.md index ceca6dc951..9a69d155a8 100644 --- a/doc/server/zio-http4s.md +++ b/doc/server/zio-http4s.md @@ -17,7 +17,7 @@ or just add the zio-http4s integration which already depends on `tapir-zio`: "com.softwaremill.sttp.tapir" %% "tapir-http4s-server-zio" % "@VERSION@" ``` -Next, instead of the usual `import sttp.tapir.*`, you should import (or extend the `ZTapir` trait, see [MyTapir](../mytapir.md)): +Next, instead of the usual `import sttp.tapir.*`, you should import (or extend the `ZTapir` trait, see [MyTapir](../other/mytapir.md)): ```scala mdoc:compile-only import sttp.tapir.ztapir.* diff --git a/doc/server/ziohttp.md b/doc/server/ziohttp.md index 6fce08f20e..ebbb82d2e0 100644 --- a/doc/server/ziohttp.md +++ b/doc/server/ziohttp.md @@ -17,7 +17,7 @@ or just add the zio-http integration which already depends on `tapir-zio`: "com.softwaremill.sttp.tapir" %% "tapir-zio-http-server" % "@VERSION@" ``` -Next, instead of the usual `import sttp.tapir.*`, you should import (or extend the `ZTapir` trait, see [MyTapir](../mytapir.md)): +Next, instead of the usual `import sttp.tapir.*`, you should import (or extend the `ZTapir` trait, see [MyTapir](../other/mytapir.md)): ```scala mdoc:compile-only import sttp.tapir.ztapir.* diff --git a/doc/support.md b/doc/support.md new file mode 100644 index 0000000000..3dbbb1b16c --- /dev/null +++ b/doc/support.md @@ -0,0 +1,11 @@ +# Support & sponsorship + +## Sponsors + +Development and maintenance of sttp tapir is sponsored by [SoftwareMill](https://softwaremill.com), a software development and consulting company. We help clients scale their business through software. We offer services around migrating and maintaining Java and Scala projects (e.g. to Java 21, or across Scala versions), ML/AI discovery workshops, introducing developer platforms (based on Kubernetes and observability technologies), and others. Our areas of expertise include performant backends, distributed systems, machine learning and data analytics, with a focus on Java, Scala, Kafka, TypeScript and Rust. + +[![](https://files.softwaremill.com/logo/logo.png "SoftwareMill")](https://softwaremill.com) + +## Commercial Support + +We offer commercial support for sttp and related technologies, as well as development services. [Contact us](https://softwaremill.com/contact/) to learn more about our offer! diff --git a/doc/tutorials/01_hello_world.md b/doc/tutorials/01_hello_world.md index fbb46c547a..848a4b1c53 100644 --- a/doc/tutorials/01_hello_world.md +++ b/doc/tutorials/01_hello_world.md @@ -97,7 +97,6 @@ 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:@VERSION@ //> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:@VERSION@ diff --git a/doc/tutorials/06_error_variants.md b/doc/tutorials/06_error_variants.md index cbfe762c5c..83308f5aea 100644 --- a/doc/tutorials/06_error_variants.md +++ b/doc/tutorials/06_error_variants.md @@ -1,5 +1,9 @@ # 6. Error variants +```{note} +The tutorial is also available [as a video](https://www.youtube.com/watch?v=w2ZL3WvhBZ8). +``` + Quite often, there's more than one thing that might go wrong. On the other hand, success can also have many facets. In the previous tutorials we've seen that Tapir includes built-in support for differentiating between successful and diff --git a/doc/tutorials/07_cats_effect.md b/doc/tutorials/07_cats_effect.md new file mode 100644 index 0000000000..0991f26a2b --- /dev/null +++ b/doc/tutorials/07_cats_effect.md @@ -0,0 +1,247 @@ +# 7. Integration with cats-effect & http4s + +[cats-effect](https://github.com/typelevel/cats-effect) is one of the most popular functional effect systems for Scala +(probably also one of the top ones when it comes to pure functional programming in general). So far we've used Tapir in +combination with so-called "direct style", where the server logic is expressed using synchronous, blocking code. + +However, one of Tapir's main strengths is that it integrates with virtually every Scala stack out there. This includes, +first and foremost, cats-effect. Let's see how we can combine the two libraries together. + +## Describing an endpoint + +We'll assume that you are familiar with what's described in the previous tutorials, especially [](01_hello_world.md). +The good news is that most of what's described there applies 1-1 to our scenario, when we want to use cats-effect. The +process of describing endpoints is identical, regardless of what programming style, Scala stack of effect library you +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:@VERSION@ + +import sttp.tapir.* + +@main def helloWorldTapir(): Unit = + val helloWorldEndpoint = endpoint + .get + .in("hello" / "world") + .in(query[String]("name")) + .out(stringBody) +``` + +```{note} +As a side note, while our previous examples required Java 21+, as they leveraged virtual threads under the hood, the +cats-effect version will work with Java 11+. +``` + +## Server-side logic + +The crucial difference when using Tapir+cats-effect, as compared to the "direct" version is in the way the server +logic is provided. The server logic does, most probably, involve side effects. Typically, this might be querying the +database, writing to Kafka, or invoking other endpoints (though in our example, here we'll constrain ourselves to good +old `println`s). Hence, any operations that the server logic performs should be captured using the `IO` monad. + +That is, given an endpoint with inputs of type `I` and error/success outputs of type `E` and `O`, the shape of the +server logic function should be `I => IO[Either[E, O]]`. In other words: given the input parameters `I`, extracted from +the request, the server logic should return a description of a computation, yielding either error `E` or success `O` +outputs, which will then be mapped to the HTTP response. + +To combine an endpoint description with the server logic, we need to use the `.serverLogic` method. While not always +required, as the type parameter is usually inferred correctly, it's nevertheless beneficial to provide the effect type +parameter explicitly, using `.serverLogic[IO]` in our case: + +{emphasize-lines="2, 4, 12-14"} +```scala +//> using dep com.softwaremill.sttp.tapir::tapir-core:@VERSION@ +//> using dep com.softwaremill.sttp.tapir::tapir-cats:@VERSION@ + +import cats.effect.IO +import sttp.tapir.* + +@main def helloWorldTapir(): Unit = + val helloWorldEndpoint = endpoint.get + .in("hello" / "world") + .in(query[String]("name")) + .out(stringBody) + .serverLogic[IO](name => IO + .println(s"Saying hello to: $name") + .flatMap(_ => IO.pure(Right(s"Hello, $name!")))) +``` + +The server-side logic consists of printing a message to the server's logs (`IO.println(...)`), followed by returning +a pure value - successful result. The `s"Hello, $name"` string that will be mapped to the response needs to be first +wrapped with a `Right` (as we want to use the successful outputs), and then lifted to an `IO` computation description +using `IO.pure`. That way, we obtain a value of type `IO[Either[Unit, String]]`, as required by the endpoint +description. + +## http4s integration + +So far we've described the shape of the endpoint, and coupled it with a function implementing the server logic, with +a matching signature. What we still need to do is to expose the endpoint using a server. + +The server must be "cats-effect-aware" - that is, it must need to know how to deal with server-side logic, which is +expressed in terms of `IO` computation descriptions. So far we've been using the `NettySyncServer`, however here it +won't be useful. Attempting to use it with our endpoint description won't compile, as there would be a mismatch on the +type used to represent effects (`Identity` vs `IO`). + +Instead, we need to use a different server. Tapir provides a couple of choices (which might be useful depending on what +you're already using in your project), but the most popular option in case of cats-effect is the +[http4s](https://http4s.org) server. That's what we're going to do as well: through a Tapir-http4s integration, called +a server interpreter. + +We've already introduced interpreters in the tutorial [](02_openapi_docs.md). In this particular case, the http4s +server interpreter will convert our endpoint description+server logic into a `HttpRoutes[IO]` type. This type is +the representation of HTTP routes that is understandable by the http4s API, and which can be used to expose a server +to the outside world. + +The conversion process is an almost-one-liner (if it wasn't for line length limit!): + +{emphasize-lines="2, 5, 7, 18-19"} +```scala +//> using dep com.softwaremill.sttp.tapir::tapir-core:@VERSION@ +//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:@VERSION@ + +import cats.effect.IO +import org.http4s.HttpRoutes +import sttp.tapir.* +import sttp.tapir.server.http4s.Http4sServerInterpreter + +@main def helloWorldTapir(): Unit = + val helloWorldEndpoint = endpoint.get + .in("hello" / "world") + .in(query[String]("name")) + .out(stringBody) + .serverLogic[IO](name => IO + .println(s"Saying hello to: $name") + .flatMap(_ => IO.pure(Right(s"Hello, $name!")))) + + val helloWorldRoutes: HttpRoutes[IO] = Http4sServerInterpreter[IO]() + .toRoutes(helloWorldEndpoint) +``` + +## Exposing the server + +As a final step, we need to expose the routes to the outside world. If you've ever used http4s, the following is fairly +standard code to start a server and handle requests until the application is interrupted or killed: + +{emphasize-lines="3, 5, 7, 8, 12, 24-30"} +```scala +//> using dep com.softwaremill.sttp.tapir::tapir-core:@VERSION@ +//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:@VERSION@ +//> using dep org.http4s::http4s-blaze-server:0.23.16 + +import cats.effect.{ExitCode, IO, IOApp} +import org.http4s.HttpRoutes +import org.http4s.blaze.server.BlazeServerBuilder +import org.http4s.server.Router +import sttp.tapir.* +import sttp.tapir.server.http4s.Http4sServerInterpreter + +object HelloWorldTapir extends IOApp: + val helloWorldEndpoint = endpoint.get + .in("hello" / "world") + .in(query[String]("name")) + .out(stringBody) + .serverLogic[IO](name => IO + .println(s"Saying hello to: $name") + .flatMap(_ => IO.pure(Right(s"Hello, $name!")))) + + val helloWorldRoutes: HttpRoutes[IO] = Http4sServerInterpreter[IO]() + .toRoutes(helloWorldEndpoint) + + override def run(args: List[String]): IO[ExitCode] = + BlazeServerBuilder[IO] + .bindHttp(8080, "localhost") + .withHttpApp(Router("/" -> helloWorldRoutes).orNotFound) + .resource + .use(_ => IO.never) + .as(ExitCode.Success) +``` + +First of all, you might notice that instead of the `@main` method, we are extending the `IOApp` trait. This is needed, +because not only our endpoint's server logic is expressed using `IO`, but the entire process of starting the server +and handling requests is described as an `IO` computation. Hence, we need to start the application in an `IO`-aware way: +the `IOApp` will handle evaluating the `IO` description and actually running the code. + +Secondly, with http4s we need to use a specific server implementation (http4s itself is only an API to define endpoints - +kind of a middle-man between Tapir and low-level networking code). We can choose from `blaze` and `ember` servers, here +we're using the `blaze` one, which is reflected in the additional dependency and the server configuration constructor: +`BlazeServerBuilder`. + +Finally, we've got the `run` method implementation, which attaches our interpreted route to the root context `/` and +exposes the server on `localhost:8080`. + +```{note} +Note that you could also attach other, non-Tapir-managed routes to the same http4s application. Tapir-interpreted +`HttpRoutes[IO]` can co-exist with routes defined in any other way. +``` + +## Adding documentation + +As a final touch, let's expose documentation using the Swagger UI, just as we did before using the Netty server. +The base process is the same: we first need to call the `SwaggerInterpreter` providing the list of endpoints, for which +documentation should be generated. However, this time we'll provide the `IO` type constructor as the type parameter. + +That way, the server logic implementing the behavior of the swagger endpoints (such as reading the .js/.css/.html +resources) will be expressed in terms of `IO`, and we'll be able to convert them later to http4s routes. And that's +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:@VERSION@ +//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:@VERSION@ +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:@VERSION@ +//> using dep org.http4s::http4s-blaze-server:0.23.16 + +import cats.effect.{ExitCode, IO, IOApp} +import cats.syntax.all.* +import org.http4s.HttpRoutes +import org.http4s.blaze.server.BlazeServerBuilder +import org.http4s.server.Router +import sttp.tapir.* +import sttp.tapir.server.http4s.Http4sServerInterpreter +import sttp.tapir.swagger.bundle.SwaggerInterpreter + +object HelloWorldTapir extends IOApp: + val helloWorldEndpoint = endpoint.get + .in("hello" / "world") + .in(query[String]("name")) + .out(stringBody) + .serverLogic[IO](name => IO + .println(s"Saying hello to: $name") + .flatMap(_ => IO.pure(Right(s"Hello, $name!")))) + + val helloWorldRoutes: HttpRoutes[IO] = Http4sServerInterpreter[IO]() + .toRoutes(helloWorldEndpoint) + + val swaggerEndpoints = SwaggerInterpreter() + .fromServerEndpoints[IO](List(helloWorldEndpoint), "My App", "1.0") + + val swaggerRoutes: HttpRoutes[IO] = Http4sServerInterpreter[IO]().toRoutes(swaggerEndpoints) + + val allRoutes: HttpRoutes[IO] = helloWorldRoutes <+> swaggerRoutes + + override def run(args: List[String]): IO[ExitCode] = + BlazeServerBuilder[IO] + .bindHttp(8080, "localhost") + .withHttpApp(Router("/" -> allRoutes).orNotFound) + .resource + .use(_ => IO.never) + .as(ExitCode.Success) +``` + +Hence, we first generate endpoint descriptions, which correspond to exposing the Swagger UI (containing the generated +OpenAPI yaml for our `/hello/world` endpoint), which use `IO` to express their server logic. Then, we interpret those +endpoints as `HttpRoutes[IO]`, which we can expose using http4's blaze server. + +## Other concepts covered so far + +We can use JSON integration, error outputs, status codes, and any other Tapir features in the same way as we did so +far with the "synchronous" server! The endpoints are described in the same way, the only thing that changes is how the +server logic is provided. + +## Further reading + +* [Netty-cats interpreter](../server/netty.md) +* [Armeria-cats interpreter](../server/armeria.md) +* [Integration with cats datatypes](../endpoint/customtypes.md) diff --git a/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala index a6b444ccf7..783c7a9d54 100644 --- a/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala @@ -1,3 +1,10 @@ +// {cat=Hello, World!; effects=Future; server=http4s}: Exposing an endpoint using the http4s server + +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.10.13 +//> using dep com.softwaremill.sttp.client3::core:3.9.7 +//> using dep org.http4s::http4s-blaze-server:0.23.16 + package sttp.tapir.examples import cats.effect.* diff --git a/examples/src/main/scala/sttp/tapir/examples/HelloWorldNettyCatsServer.scala b/examples/src/main/scala/sttp/tapir/examples/HelloWorldNettyCatsServer.scala index 0ba70e423c..3812edd5c7 100644 --- a/examples/src/main/scala/sttp/tapir/examples/HelloWorldNettyCatsServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/HelloWorldNettyCatsServer.scala @@ -1,3 +1,9 @@ +// {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.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-cats:1.10.13 +//> using dep com.softwaremill.sttp.client3::core:3.9.7 + package sttp.tapir.examples import cats.effect.{IO, IOApp} diff --git a/examples/src/main/scala/sttp/tapir/examples/HelloWorldZioHttpServer.scala b/examples/src/main/scala/sttp/tapir/examples/HelloWorldZioHttpServer.scala index 53c6bbf1ca..56c122637b 100644 --- a/examples/src/main/scala/sttp/tapir/examples/HelloWorldZioHttpServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/HelloWorldZioHttpServer.scala @@ -1,3 +1,9 @@ +// {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.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-json-zio:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-zio-http-server:1.10.13 + package sttp.tapir.examples import sttp.tapir.PublicEndpoint diff --git a/examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala index 35af2d0e8f..7001452c51 100644 --- a/examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala @@ -1,3 +1,14 @@ +// {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.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server-zio:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-zio:1.10.13 +//> using dep org.http4s::http4s-blaze-server:0.23.16 +//> using dep dev.zio::zio-interop-cats:23.1.0.2 + package sttp.tapir.examples import cats.syntax.all.* diff --git a/examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala index 4ae638389f..f8f2248bfe 100644 --- a/examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala @@ -1,3 +1,13 @@ +// {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.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server-zio:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-zio:1.10.13 +//> using dep org.http4s::http4s-blaze-server:0.23.16 +//> using dep dev.zio::zio-interop-cats:23.1.0.2 + package sttp.tapir.examples import cats.syntax.all.* diff --git a/examples/src/main/scala/sttp/tapir/examples/ZioExampleZioHttpServer.scala b/examples/src/main/scala/sttp/tapir/examples/ZioExampleZioHttpServer.scala index f3d49a2264..50fd85f19d 100644 --- a/examples/src/main/scala/sttp/tapir/examples/ZioExampleZioHttpServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/ZioExampleZioHttpServer.scala @@ -1,3 +1,14 @@ +// {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.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-zio-http-server:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-zio:1.10.13 +//> using dep org.http4s::http4s-blaze-server:0.23.16 +//> using dep dev.zio::zio-interop-cats:23.1.0.2 + package sttp.tapir.examples import io.circe.generic.auto.* diff --git a/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala b/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala index 02618e483b..a1004853db 100644 --- a/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala +++ b/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala @@ -1,3 +1,12 @@ +// {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.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server-zio:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-zio:1.10.13 +//> using dep org.http4s::http4s-blaze-server:0.23.16 +//> using dep com.softwaremill.sttp.client3::async-http-client-backend-zio:3.9.7 + package sttp.tapir.examples import org.http4s.* diff --git a/examples/src/main/scala/sttp/tapir/examples/booksExample.scala b/examples/src/main/scala/sttp/tapir/examples/booksExample.scala index 77ff388654..e183e77e7f 100644 --- a/examples/src/main/scala/sttp/tapir/examples/booksExample.scala +++ b/examples/src/main/scala/sttp/tapir/examples/booksExample.scala @@ -1,10 +1,20 @@ +// {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.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-sttp-client:1.10.13 +//> using dep org.apache.pekko::pekko-http:1.0.1 +//> using dep org.apache.pekko::pekko-stream:1.0.3 + package sttp.tapir.examples import sttp.tapir.generic.auto.* @main def booksExample(): Unit = import org.slf4j.{Logger, LoggerFactory} - val logger: Logger = LoggerFactory.getLogger(getClass().getName) + val logger: Logger = LoggerFactory.getLogger(this.getClass().getName) type Limit = Option[Int] type AuthToken = String @@ -115,7 +125,7 @@ import sttp.tapir.generic.auto.* // interpreting the endpoint description and converting it to an pekko-http route, providing the logic which // should be run when the endpoint is invoked. List( - addBook.serverLogic((bookAddLogic _).tupled), + addBook.serverLogic(bookAddLogic.tupled), booksListing.serverLogic(bookListingLogic), booksListingByGenre.serverLogic(bookListingByGenreLogic) ) diff --git a/examples/src/main/scala/sttp/tapir/examples/booksPicklerExample.scala b/examples/src/main/scala/sttp/tapir/examples/booksPicklerExample.scala index 055c4375ed..087f0ddf3b 100644 --- a/examples/src/main/scala/sttp/tapir/examples/booksPicklerExample.scala +++ b/examples/src/main/scala/sttp/tapir/examples/booksPicklerExample.scala @@ -1,3 +1,13 @@ +// {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.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-json-pickler:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-sttp-client:1.10.13 +//> using dep org.apache.pekko::pekko-http:1.0.1 +//> using dep org.apache.pekko::pekko-stream:1.0.3 + package sttp.tapir.examples import sttp.tapir.server.netty.{NettyFutureServer, NettyFutureServerBinding} @@ -7,7 +17,7 @@ import scala.concurrent.duration.Duration @main def booksPicklerExample(): Unit = import org.slf4j.{Logger, LoggerFactory} - val logger: Logger = LoggerFactory.getLogger(getClass.getName) + val logger: Logger = LoggerFactory.getLogger(this.getClass.getName) type Limit = Option[Int] type AuthToken = String @@ -121,7 +131,7 @@ import scala.concurrent.duration.Duration // interpreting the endpoint description and converting it to an akka-http route, providing the logic which // should be run when the endpoint is invoked. List( - addBook.serverLogic((bookAddLogic _).tupled), + addBook.serverLogic(bookAddLogic.tupled), booksListing.serverLogic(bookListingLogic), booksListingByGenre.serverLogic(bookListingByGenreLogic) ) 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 076dbd3041..3ea8bfc92d 100644 --- a/examples/src/main/scala/sttp/tapir/examples/client/Http4sClientExample.scala +++ b/examples/src/main/scala/sttp/tapir/examples/client/Http4sClientExample.scala @@ -1,3 +1,12 @@ +// {cat=Client interpreter; effects=cats-effect; JSON=circe}: Interpreting an endpoint as an http4s client + +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-http4s-client:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.10.13 +//> 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 + package sttp.tapir.examples.client import cats.effect.{ExitCode, IO, IOApp} @@ -6,11 +15,10 @@ import sttp.tapir.* import sttp.tapir.client.http4s.Http4sClientInterpreter import sttp.tapir.generic.auto.* import sttp.tapir.json.circe.* - import org.slf4j.{Logger, LoggerFactory} -val logger: Logger = LoggerFactory.getLogger(getClass.getName) object Http4sClientExample extends IOApp: + val logger: Logger = LoggerFactory.getLogger(this.getClass.getName) case class User(id: Int, name: String) 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 9a244fd469..22489b2bb4 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,3 +1,8 @@ +// {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.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.10.13 + package sttp.tapir.examples.custom_types import io.circe.generic.auto.* 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 ba442caeec..f0e2cac31c 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,20 @@ +// {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.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-sttp-client:1.10.13 +//> using dep org.apache.pekko::pekko-http:1.0.1 +//> using dep org.apache.pekko::pekko-stream:1.0.3 + package sttp.tapir.examples.custom_types import sttp.tapir.Schema @main def booksExampleSemiauto(): Unit = import org.slf4j.{Logger, LoggerFactory} - val logger: Logger = LoggerFactory.getLogger(getClass().getName) + val logger: Logger = LoggerFactory.getLogger(this.getClass().getName) type Limit = Option[Int] type AuthToken = String @@ -124,7 +134,7 @@ import sttp.tapir.Schema // interpreting the endpoint description and converting it to an akka-http route, providing the logic which // should be run when the endpoint is invoked. List( - addBook.serverLogic((bookAddLogic _).tupled), + addBook.serverLogic(bookAddLogic.tupled), booksListing.serverLogic(bookListingLogic), booksListingByGenre.serverLogic(bookListingByGenreLogic) ) 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 1a87bfea52..2690f377e9 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,3 +1,9 @@ +// {cat=Custom types; effects=Direct; server=Netty; docs=Swagger UI}: Handling comma-separated query parameters + +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.10.13 + package sttp.tapir.examples.custom_types import ox.supervised 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 9ac37e6386..894bf6cdc9 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,3 +1,10 @@ +// {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.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.10.13 + package sttp.tapir.examples.custom_types import io.circe.Codec as CirceCodec diff --git a/examples/src/main/scala/sttp/tapir/examples/ErrorUnionTypesHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/errors/ErrorUnionTypesHttp4sServer.scala similarity index 87% rename from examples/src/main/scala/sttp/tapir/examples/ErrorUnionTypesHttp4sServer.scala rename to examples/src/main/scala/sttp/tapir/examples/errors/ErrorUnionTypesHttp4sServer.scala index 9c5d437a4a..b56845d6a0 100644 --- a/examples/src/main/scala/sttp/tapir/examples/ErrorUnionTypesHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/errors/ErrorUnionTypesHttp4sServer.scala @@ -1,19 +1,26 @@ -package sttp.tapir.examples +// {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.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.10.13 +//> using dep org.http4s::http4s-blaze-server:0.23.16 +//> using dep com.softwaremill.sttp.client3::core:3.9.7 + +package sttp.tapir.examples.errors import cats.effect.* -import cats.syntax.all.* import io.circe.generic.auto.* import org.http4s.HttpRoutes -import org.http4s.server.Router import org.http4s.blaze.server.BlazeServerBuilder +import org.http4s.server.Router import sttp.client3.* import sttp.model.StatusCode import sttp.shared.Identity import sttp.tapir.* -import sttp.tapir.server.http4s.Http4sServerInterpreter -import sttp.tapir.json.circe.* import sttp.tapir.generic.auto.* +import sttp.tapir.json.circe.* import sttp.tapir.server.ServerEndpoint +import sttp.tapir.server.http4s.Http4sServerInterpreter import scala.concurrent.ExecutionContext 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 2a97c02f58..e05bff64cd 100644 --- a/examples/src/main/scala/sttp/tapir/examples/errors/IronRefinementErrorsNettyServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/errors/IronRefinementErrorsNettyServer.scala @@ -1,3 +1,11 @@ +// {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.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-cats:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-iron:1.10.13 +//> using dep com.softwaremill.sttp.client3::core:3.9.7 + package sttp.tapir.examples.errors import cats.effect.{IO, IOApp} 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 07f86310b8..ff8fff9da9 100644 --- a/examples/src/main/scala/sttp/tapir/examples/errors/customErrorsOnDecodeFailurePekkoServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/errors/customErrorsOnDecodeFailurePekkoServer.scala @@ -1,9 +1,16 @@ +// {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.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.10.13 +//> 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 + package sttp.tapir.examples.errors import org.apache.pekko.actor.ActorSystem import org.apache.pekko.http.scaladsl.Http import org.apache.pekko.http.scaladsl.server.Route -import ox.discard import sttp.shared.Identity import sttp.tapir.* import sttp.tapir.server.pekkohttp.{PekkoHttpServerInterpreter, PekkoHttpServerOptions} @@ -62,4 +69,4 @@ import sttp.tapir.server.interceptor.decodefailure.{DecodeFailureHandler, Defaul binding } - Await.result(bindAndCheck.flatMap(_.terminate(1.minute)), 1.minute).discard + val _ = Await.result(bindAndCheck.flatMap(_.terminate(1.minute)), 1.minute) 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 7a0972c377..19cfc9eb71 100644 --- a/examples/src/main/scala/sttp/tapir/examples/errors/errorOutputsPekkoServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/errors/errorOutputsPekkoServer.scala @@ -1,9 +1,17 @@ +// {cat=Error handling; effects=Future; server=Pekko HTTP; json=circe}: Error and successful outputs + +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.10.13 +//> 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 + package sttp.tapir.examples.errors import org.apache.pekko.actor.ActorSystem import org.apache.pekko.http.scaladsl.Http import org.apache.pekko.http.scaladsl.server.Route -import ox.discard import sttp.client3.* import sttp.shared.Identity import sttp.tapir.generic.auto.* @@ -50,4 +58,4 @@ import scala.concurrent.{Await, Future} binding } - Await.result(bindAndCheck.flatMap(_.terminate(1.minute)), 1.minute).discard + val _ = Await.result(bindAndCheck.flatMap(_.terminate(1.minute)), 1.minute) diff --git a/examples/src/main/scala/sttp/tapir/examples/helloWorldArmeriaServer.scala b/examples/src/main/scala/sttp/tapir/examples/helloWorldArmeriaServer.scala index bfadc347f5..fc915718f3 100644 --- a/examples/src/main/scala/sttp/tapir/examples/helloWorldArmeriaServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/helloWorldArmeriaServer.scala @@ -1,9 +1,14 @@ +// {cat=Hello, World!; effects=Future; server=Armeria}: Exposing an endpoint using the Armeria server + +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-armeria-server:1.10.13 +//> using dep com.softwaremill.sttp.client3::core:3.9.7 + package sttp.tapir.examples import com.linecorp.armeria.server.Server import sttp.capabilities.armeria.ArmeriaStreams import sttp.client3.{HttpURLConnectionBackend, Identity, SttpBackend, UriContext, asStringAlways, basicRequest} -import sttp.shared.Identity import sttp.tapir.server.armeria.{ArmeriaFutureServerInterpreter, TapirService} import sttp.tapir.* diff --git a/examples/src/main/scala/sttp/tapir/examples/helloWorldJdkHttpServer.scala b/examples/src/main/scala/sttp/tapir/examples/helloWorldJdkHttpServer.scala index d466dd7b6f..a274f1585d 100644 --- a/examples/src/main/scala/sttp/tapir/examples/helloWorldJdkHttpServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/helloWorldJdkHttpServer.scala @@ -1,3 +1,9 @@ +// {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.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-jdkhttp-server:1.10.13 +//> using dep com.softwaremill.sttp.client3::core:3.9.7 + package sttp.tapir.examples import sttp.client3.{HttpURLConnectionBackend, Response, SttpBackend, UriContext, asStringAlways, basicRequest} diff --git a/examples/src/main/scala/sttp/tapir/examples/helloWorldNettyFutureServer.scala b/examples/src/main/scala/sttp/tapir/examples/helloWorldNettyFutureServer.scala index efd8a53ac2..c1e1515261 100644 --- a/examples/src/main/scala/sttp/tapir/examples/helloWorldNettyFutureServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/helloWorldNettyFutureServer.scala @@ -1,3 +1,9 @@ +// {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.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server:1.10.13 +//> using dep com.softwaremill.sttp.client3::core:3.9.7 + package sttp.tapir.examples import sttp.client3.{HttpURLConnectionBackend, SttpBackend, UriContext, asStringAlways, basicRequest} diff --git a/examples/src/main/scala/sttp/tapir/examples/helloWorldNettySyncServer.scala b/examples/src/main/scala/sttp/tapir/examples/helloWorldNettySyncServer.scala index 537d176bd4..9be7794798 100644 --- a/examples/src/main/scala/sttp/tapir/examples/helloWorldNettySyncServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/helloWorldNettySyncServer.scala @@ -1,3 +1,9 @@ +// {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.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.10.13 +//> using dep com.softwaremill.sttp.client3::core:3.9.7 + package sttp.tapir.examples import ox.* diff --git a/examples/src/main/scala/sttp/tapir/examples/helloWorldPekkoServer.scala b/examples/src/main/scala/sttp/tapir/examples/helloWorldPekkoServer.scala index b5fe8e1603..e83879b5db 100644 --- a/examples/src/main/scala/sttp/tapir/examples/helloWorldPekkoServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/helloWorldPekkoServer.scala @@ -1,3 +1,9 @@ +// {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.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.10.13 +//> using dep com.softwaremill.sttp.client3::core:3.9.7 + package sttp.tapir.examples import org.apache.pekko.actor.ActorSystem 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 4d75eeac56..25dd21a962 100644 --- a/examples/src/main/scala/sttp/tapir/examples/logging/ZioLoggingWithCorrelationIdNettyServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/logging/ZioLoggingWithCorrelationIdNettyServer.scala @@ -1,3 +1,9 @@ +// {cat=Logging; effects=ZIO; server=Netty}: Logging using a correlation id + +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-zio:1.10.13 +//> using dep com.softwaremill.sttp.client3::zio:3.9.7 + package sttp.tapir.examples.logging import sttp.client3.httpclient.zio.HttpClientZioBackend @@ -30,7 +36,7 @@ object ZioLoggingWithCorrelationIdNettyServer extends ZIOAppDefault: } }) - override def run: URIO[Any, ExitCode] = + override def run: URIO[Any, ExitCode] = val serverOptions = NettyZioServerOptions.customiseInterceptors.prependInterceptor(correlationIdInterceptor).options (for { binding <- NettyZioServer(serverOptions).port(8080).addEndpoint(loggingEndpoint).start() 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 424157c8d8..547d8f3f1f 100644 --- a/examples/src/main/scala/sttp/tapir/examples/multipart/multipartFormUploadPekkoServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/multipart/multipartFormUploadPekkoServer.scala @@ -1,3 +1,11 @@ +// {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.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.10.13 +//> 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 + package sttp.tapir.examples.multipart import java.io.PrintWriter @@ -5,7 +13,6 @@ import java.io.PrintWriter import org.apache.pekko.actor.ActorSystem import org.apache.pekko.http.scaladsl.Http import org.apache.pekko.http.scaladsl.server.Route -import ox.discard import sttp.client3.* import sttp.shared.Identity import sttp.tapir.generic.auto.* @@ -62,4 +69,4 @@ import scala.concurrent.duration.* binding } - Await.result(bindAndCheck.flatMap(_.terminate(1.minute)), 1.minute).discard + val _ = Await.result(bindAndCheck.flatMap(_.terminate(1.minute)), 1.minute) 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 06260ce091..edda1af348 100644 --- a/examples/src/main/scala/sttp/tapir/examples/observability/ZioMetricsExample.scala +++ b/examples/src/main/scala/sttp/tapir/examples/observability/ZioMetricsExample.scala @@ -1,3 +1,9 @@ +// {cat=Observability; effects=ZIO; server=ZIO HTTP}: Reporting Prometheus metrics + +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-zio-http-server:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-zio-metrics:1.10.13 + package sttp.tapir.examples.observability import sttp.tapir.* @@ -27,16 +33,14 @@ object ZioMetricsExample extends ZIOAppDefault: val metricsInterceptor: MetricsRequestInterceptor[Task] = metrics.metricsInterceptor() // noinspection DuplicatedCode - override def run: ZIO[Any with ZIOAppArgs with Scope, Any, Any] = + override def run: ZIO[Any & ZIOAppArgs & Scope, Any, Any] = val serverOptions: ZioHttpServerOptions[Any] = ZioHttpServerOptions.customiseInterceptors.metricsInterceptor(metricsInterceptor).options val app: Routes[Any, ZioHttpResponse] = ZioHttpInterpreter(serverOptions).toHttp(all) - val port = sys.env.get("http.port").map(_.toInt).getOrElse(8080) - (for { serverPort <- Server.install(app) - _ <- Console.printLine(s"Server started at http://localhost:${serverPort}. Press ENTER key to exit.") + _ <- Console.printLine(s"Server started at http://localhost:$serverPort. Press ENTER key to exit.") _ <- Console.readLine } yield serverPort) .provide( 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 cf487b2105..c9108d3f8e 100644 --- a/examples/src/main/scala/sttp/tapir/examples/observability/datadogMetricsExample.scala +++ b/examples/src/main/scala/sttp/tapir/examples/observability/datadogMetricsExample.scala @@ -1,3 +1,11 @@ +// {cat=Observability; effects=Future; server=Netty; json=circe}: Reporting DataDog metrics + +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-datadog-metrics:1.10.13 +//> using dep org.slf4j:slf4j-api:2.0.13 + package sttp.tapir.examples.observability import com.timgroup.statsd.NonBlockingStatsDClientBuilder @@ -15,9 +23,10 @@ import scala.concurrent.{Await, Future} import scala.io.StdIn import org.slf4j.{Logger, LoggerFactory} -val logger: Logger = LoggerFactory.getLogger(getClass.getName) @main def datadogMetricsExample(): Unit = + val logger: Logger = LoggerFactory.getLogger(this.getClass.getName) + case class Person(name: String) // Simple endpoint returning 200 or 400 response with string body 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 a859631b39..7fa39885dc 100644 --- a/examples/src/main/scala/sttp/tapir/examples/observability/openTelemetryMetricsExample.scala +++ b/examples/src/main/scala/sttp/tapir/examples/observability/openTelemetryMetricsExample.scala @@ -1,3 +1,12 @@ +// {cat=Observability; effects=Future; server=Netty; json=circe}: Reporting OpenTelemetry metrics + +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-opentelemetry-metrics:1.10.13 +//> using dep io.opentelemetry:opentelemetry-exporter-otlp:1.40.0 +//> using dep org.slf4j:slf4j-api:2.0.13 + package sttp.tapir.examples.observability import io.circe.generic.auto.* @@ -12,6 +21,7 @@ import sttp.tapir.json.circe.jsonBody import sttp.tapir.server.ServerEndpoint import sttp.tapir.server.metrics.opentelemetry.OpenTelemetryMetrics import sttp.tapir.server.netty.{NettyFutureServer, NettyFutureServerOptions} +import org.slf4j.{Logger, LoggerFactory} import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration.* @@ -57,8 +67,7 @@ import scala.io.StdIn * }}} */ @main def openTelemetryMetricsExample(): Unit = - import org.slf4j.{Logger, LoggerFactory} - val logger: Logger = LoggerFactory.getLogger(getClass().getName) + val logger: Logger = LoggerFactory.getLogger(this.getClass().getName) case class Person(name: String) 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 76b142ecce..3c10916578 100644 --- a/examples/src/main/scala/sttp/tapir/examples/observability/prometheusMetricsExample.scala +++ b/examples/src/main/scala/sttp/tapir/examples/observability/prometheusMetricsExample.scala @@ -1,3 +1,11 @@ +// {cat=Observability; effects=Future; server=Netty; json=circe}: Reporting Prometheus metrics + +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-prometheus-metrics:1.10.13 +//> using dep org.slf4j:slf4j-api:2.0.13 + package sttp.tapir.examples.observability import io.circe.generic.auto.* @@ -7,6 +15,7 @@ import sttp.tapir.json.circe.* import sttp.tapir.server.ServerEndpoint import sttp.tapir.server.metrics.prometheus.PrometheusMetrics import sttp.tapir.server.netty.{NettyFutureServer, NettyFutureServerOptions} +import org.slf4j.{Logger, LoggerFactory} import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration.* @@ -14,8 +23,7 @@ import scala.concurrent.{Await, Future} import scala.io.StdIn @main def prometheusMetricsExample(): Unit = - import org.slf4j.{Logger, LoggerFactory} - val logger: Logger = LoggerFactory.getLogger(getClass().getName) + val logger: Logger = LoggerFactory.getLogger(this.getClass().getName) case class Person(name: String) 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 b954caba00..af7dc3be23 100644 --- a/examples/src/main/scala/sttp/tapir/examples/openapi/MultipleEndpointsDocumentationHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/openapi/MultipleEndpointsDocumentationHttp4sServer.scala @@ -1,3 +1,11 @@ +// {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.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.10.13 +//> using dep org.http4s::http4s-blaze-server:0.23.16 + package sttp.tapir.examples.openapi import cats.effect.* @@ -63,7 +71,7 @@ object MultipleEndpointsDocumentationHttp4sServer extends IOApp: val routes: HttpRoutes[IO] = booksListingRoutes <+> addBookRoutes <+> swaggerUIRoutes - override def run(args: List[String]): IO[ExitCode] = + override def run(args: List[String]): IO[ExitCode] = // starting the server BlazeServerBuilder[IO] .withExecutionContext(ec) @@ -78,4 +86,3 @@ object MultipleEndpointsDocumentationHttp4sServer extends IOApp: } } .as(ExitCode.Success) - 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 0addec9329..4bd63888ba 100644 --- a/examples/src/main/scala/sttp/tapir/examples/openapi/RedocContextPathHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/openapi/RedocContextPathHttp4sServer.scala @@ -1,3 +1,10 @@ +// {cat=OpenAPI documentation; effects=cats-effect; server=http4s; docs=ReDoc}: Exposing documentation using ReDoc + +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-redoc-bundle:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.10.13 +//> using dep org.http4s::http4s-blaze-server:0.23.16 + package sttp.tapir.examples.openapi import cats.effect.* 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 517bcbb153..c9e344b769 100644 --- a/examples/src/main/scala/sttp/tapir/examples/openapi/RedocZioHttpServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/openapi/RedocZioHttpServer.scala @@ -1,3 +1,10 @@ +// {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.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-redoc-bundle:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-zio-http-server:1.10.13 + package sttp.tapir.examples.openapi import io.circe.generic.auto.* 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 c45cf28c0f..2d285c7331 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,15 @@ +// {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.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.10.13 + package sttp.tapir.examples.openapi import java.util.concurrent.atomic.AtomicReference import org.apache.pekko.actor.ActorSystem import org.apache.pekko.http.scaladsl.Http -import ox.discard import io.circe.generic.auto.* import sttp.tapir.generic.auto.* import sttp.tapir.* @@ -76,4 +82,4 @@ import scala.concurrent.{Await, Future} } // cleanup - Await.result(bindAndCheck.flatMap(_.terminate(1.minute)), 1.minute).discard + val _ = Await.result(bindAndCheck.flatMap(_.terminate(1.minute)), 1.minute) 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 d8e70756d7..c5e0c460da 100644 --- a/examples/src/main/scala/sttp/tapir/examples/openapi/openapiExtensions.scala +++ b/examples/src/main/scala/sttp/tapir/examples/openapi/openapiExtensions.scala @@ -1,3 +1,10 @@ +// {cat=OpenAPI documentation; json=circe}: Adding OpenAPI documentation extensions + +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-openapi-docs:1.10.13 +//> using dep com.softwaremill.sttp.apispec::openapi-circe-yaml:0.10.0 + package sttp.tapir.examples.openapi import io.circe.generic.auto.* 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 4ec59601b2..c3ebed52d9 100644 --- a/examples/src/main/scala/sttp/tapir/examples/openapi/swaggerUIOAuth2PekkoServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/openapi/swaggerUIOAuth2PekkoServer.scala @@ -1,3 +1,9 @@ +// {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.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.10.13 + package sttp.tapir.examples.openapi import org.apache.pekko.actor.ActorSystem 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 af95ba150b..3f3ca93730 100644 --- a/examples/src/main/scala/sttp/tapir/examples/schema/customisingSchemas.scala +++ b/examples/src/main/scala/sttp/tapir/examples/schema/customisingSchemas.scala @@ -1,3 +1,10 @@ +// {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.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.10.13 + package sttp.tapir.examples.schema import io.circe.generic.auto.* 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 53e6b747cf..0870e64b4a 100644 --- a/examples/src/main/scala/sttp/tapir/examples/security/OAuth2GithubHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/security/OAuth2GithubHttp4sServer.scala @@ -1,3 +1,12 @@ +// {cat=Security; effects=cats-effect; server=http4s; json=circe}: Login using OAuth2, authorization code flow + +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.10.13 +//> using dep com.softwaremill.sttp.client3::async-http-client-backend-cats:3.9.7 +//> using dep org.http4s::http4s-blaze-server:0.23.16 +//> using dep com.github.jwt-scala::jwt-circe:10.0.1 + package sttp.tapir.examples.security import cats.effect.* 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 bd42b8413b..0b4d277970 100644 --- a/examples/src/main/scala/sttp/tapir/examples/security/ServerSecurityLogicZio.scala +++ b/examples/src/main/scala/sttp/tapir/examples/security/ServerSecurityLogicZio.scala @@ -1,3 +1,9 @@ +// {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.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-zio-http-server:1.10.13 +//> using dep com.softwaremill.sttp.client3::async-http-client-backend-zio:3.9.7 + package sttp.tapir.examples.security import sttp.client3._ @@ -39,7 +45,7 @@ object ServerSecurityLogicZio extends ZIOAppDefault: .in("hello") .in(query[String]("salutation")) .out(stringBody) - .mapErrorOut(AuthenticationHelloError)(_.wrapped) + .mapErrorOut(AuthenticationHelloError.apply)(_.wrapped) // returning a 400 with the "why" field from the exception .errorOutVariant[HelloError](oneOfVariant(stringBody.mapTo[NoHelloError])) // defining the remaining server logic (which uses the authenticated user) 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 dc7843a681..bed081b213 100644 --- a/examples/src/main/scala/sttp/tapir/examples/security/basicAuthenticationPekkoServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/security/basicAuthenticationPekkoServer.scala @@ -1,3 +1,9 @@ +// {cat=Security; effects=Future; server=Pekko HTTP}: HTTP basic authentication + +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.10.13 +//> using dep com.softwaremill.sttp.client3::core:3.9.7 + package sttp.tapir.examples.security import org.apache.pekko.actor.ActorSystem @@ -45,4 +51,4 @@ import scala.concurrent.{Await, Future} binding } - Await.result(bindAndCheck.flatMap(_.terminate(1.minute)), 1.minute) + val _ = Await.result(bindAndCheck.flatMap(_.terminate(1.minute)), 1.minute) 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 e50d3924b0..8a1e19bc0f 100644 --- a/examples/src/main/scala/sttp/tapir/examples/security/corsInterceptorPekkoServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/security/corsInterceptorPekkoServer.scala @@ -1,3 +1,9 @@ +// {cat=Security; effects=Future; server=Pekko HTTP}: CORS interceptor + +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.10.13 +//> using dep com.softwaremill.sttp.client3::core:3.9.7 + package sttp.tapir.examples.security import org.apache.pekko.actor.ActorSystem 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 e68eaa7775..d5e9058d08 100644 --- a/examples/src/main/scala/sttp/tapir/examples/security/externalSecurityInterceptor.scala +++ b/examples/src/main/scala/sttp/tapir/examples/security/externalSecurityInterceptor.scala @@ -1,3 +1,9 @@ +// {cat=Security; effects=Future; server=Netty}: Interceptor verifying externally added security credentials + +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server:1.10.13 +//> using dep com.softwaremill.sttp.client3::core:3.9.7 + package sttp.tapir.examples.security import sttp.client3.* 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 956cc89ced..cfbcc01cfa 100644 --- a/examples/src/main/scala/sttp/tapir/examples/security/serverSecurityLogicPekko.scala +++ b/examples/src/main/scala/sttp/tapir/examples/security/serverSecurityLogicPekko.scala @@ -1,3 +1,9 @@ +// {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.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.10.13 +//> using dep com.softwaremill.sttp.client3::core:3.9.7 + package sttp.tapir.examples.security import org.apache.pekko.actor.ActorSystem @@ -47,7 +53,7 @@ import scala.concurrent.{Await, Future} .in("hello") .in(query[String]("salutation")) .out(stringBody) - .mapErrorOut(AuthenticationHelloError)(_.wrapped) + .mapErrorOut(AuthenticationHelloError.apply)(_.wrapped) // returning a 400 with the "why" field from the exception .errorOutVariant[HelloError](oneOfVariant(stringBody.mapTo[NoHelloError])) // defining the remaining server logic (which uses the authenticated user) @@ -88,4 +94,4 @@ import scala.concurrent.{Await, Future} binding } - Await.result(bindAndCheck.flatMap(_.terminate(1.minute)), 1.minute) + val _ = Await.result(bindAndCheck.flatMap(_.terminate(1.minute)), 1.minute) 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 50fa7e67cf..50be32279f 100644 --- a/examples/src/main/scala/sttp/tapir/examples/security/serverSecurityLogicRefreshCookiesPekko.scala +++ b/examples/src/main/scala/sttp/tapir/examples/security/serverSecurityLogicRefreshCookiesPekko.scala @@ -1,3 +1,9 @@ +// {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.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.10.13 +//> using dep com.softwaremill.sttp.client3::core:3.9.7 + package sttp.tapir.examples.security import org.apache.pekko.actor.ActorSystem @@ -69,4 +75,4 @@ import scala.concurrent.{Await, Future} binding } - Await.result(bindAndCheck.flatMap(_.terminate(1.minute)), 1.minute) + val _ = Await.result(bindAndCheck.flatMap(_.terminate(1.minute)), 1.minute) 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 df286a76bd..ef970ba90e 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,3 +1,9 @@ +// {cat=Static content; effects=Direct; server=Netty}: Serving static files from a directory + +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-files:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.10.13 + package sttp.tapir.examples.static_content import sttp.shared.Identity 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 e20fe753bb..b511c4ab57 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,3 +1,10 @@ +// {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.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-files:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.10.13 +//> using dep com.softwaremill.sttp.client3::core:3.9.7 + package sttp.tapir.examples.static_content import org.apache.pekko.actor.ActorSystem @@ -52,4 +59,4 @@ import scala.concurrent.{Await, Future} binding } - Await.result(bindAndCheck.flatMap(_.terminate(1.minute)), 1.minute) + val _ = Await.result(bindAndCheck.flatMap(_.terminate(1.minute)), 1.minute) 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 385868090f..6942d82cfb 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,3 +1,9 @@ +// {cat=Static content; effects=Future; server=Pekko HTTP}: Serving static files from resources + +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-files:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.10.13 + package sttp.tapir.examples.static_content import org.apache.pekko.actor.ActorSystem 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 652f7e7a9b..b4ff8c5a9f 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,3 +1,10 @@ +// {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.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-files:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.10.13 +//> using dep com.softwaremill.sttp.client3::core:3.9.7 + package sttp.tapir.examples.static_content import org.apache.pekko.actor.ActorSystem 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 dfa215d2d0..fd32c6c34c 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,3 +1,8 @@ +// {cat=Status code; effects=Direct; server=Netty}: Serving static files from a directory + +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.10.13 + package sttp.tapir.examples.status_code import ox.supervised 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 879649db2a..5269b000db 100644 --- a/examples/src/main/scala/sttp/tapir/examples/streaming/ProxyHttp4sFs2Server.scala +++ b/examples/src/main/scala/sttp/tapir/examples/streaming/ProxyHttp4sFs2Server.scala @@ -1,3 +1,10 @@ +// {cat=Streaming; effects=cats-effect; server=http4s}: Proxy requests, handling bodies as fs2 streams + +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.10.13 +//> using dep com.softwaremill.sttp.client3::fs2:3.9.7 +//> using dep org.http4s::http4s-blaze-server:0.23.16 + package sttp.tapir.examples.streaming import cats.effect.{ExitCode, IO, IOApp} @@ -15,7 +22,7 @@ import sttp.tapir.server.http4s.Http4sServerInterpreter /** Proxies requests from /proxy to https://httpbin.org/anything */ object ProxyHttp4sFs2Server extends IOApp: import org.slf4j.{Logger, LoggerFactory} - val logger: Logger = LoggerFactory.getLogger(getClass().getName) + val logger: Logger = LoggerFactory.getLogger(this.getClass().getName) val proxyEndpoint: PublicEndpoint[ (Method, List[String], QueryParams, List[Header], Stream[IO, Byte]), 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 18eadf1a8b..9ce3711077 100644 --- a/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingHttp4sFs2Server.scala +++ b/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingHttp4sFs2Server.scala @@ -1,3 +1,10 @@ +// {cat=Streaming; effects=cats-effect; server=http4s}: Stream response as an fs2 stream + +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.10.13 +//> using dep com.softwaremill.sttp.client3::core:3.9.7 +//> using dep org.http4s::http4s-blaze-server:0.23.16 + package sttp.tapir.examples.streaming import cats.effect.{ExitCode, IO, IOApp} 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 c5fada2682..acaa984bf4 100644 --- a/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingNettyFs2Server.scala +++ b/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingNettyFs2Server.scala @@ -1,3 +1,9 @@ +// {cat=Streaming; effects=cats-effect; server=Netty}: Stream response as an fs2 stream + +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-cats:1.10.13 +//> using dep com.softwaremill.sttp.client3::core:3.9.7 + package sttp.tapir.examples.streaming import cats.effect.{ExitCode, IO, IOApp} @@ -30,7 +36,7 @@ object StreamingNettyFs2Server extends IOApp: Stream .emit(List[Char]('a', 'b', 'c', 'd')) .repeat - .flatMap(list => Stream.chunk(Chunk.seq(list))) + .flatMap(list => Stream.chunk(Chunk.from(list))) .metered[IO](100.millis) .take(size) .covary[IO] @@ -56,8 +62,6 @@ object StreamingNettyFs2Server extends IOApp: startServer .map { binding => - val port = binding.port - val host = binding.hostName println(s"Server started at port = ${binding.port}") val backend: SttpBackend[Identity, Any] = HttpClientSyncBackend() 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 469563a166..159464e33a 100644 --- a/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingNettyZioServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingNettyZioServer.scala @@ -1,3 +1,9 @@ +// {cat=Streaming; effects=ZIO; server=Netty}: Stream response as a ZIO stream + +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-zio:1.10.13 +//> using dep com.softwaremill.sttp.client3::core:3.9.7 + package sttp.tapir.examples.streaming import sttp.capabilities.zio.ZioStreams @@ -46,8 +52,6 @@ object StreamingNettyZioServer extends ZIOAppDefault: .addEndpoint(serverEndpoint) .start() _ = { - val port = binding.port - val host = binding.hostName println(s"Server started at port = ${binding.port}") val backend: SttpBackend[Identity, Any] = HttpClientSyncBackend() 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 33c2e74676..bc5f16eeca 100644 --- a/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingZioHttpServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingZioHttpServer.scala @@ -1,3 +1,9 @@ +// {cat=Streaming; effects=ZIO; server=ZIO HTTP}: Stream response as a ZIO stream + +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-zio-http-server:1.10.13 +//> using dep com.softwaremill.sttp.client3::core:3.9.7 + package sttp.tapir.examples.streaming import sttp.capabilities.zio.ZioStreams 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 9c462ca4a2..c001228efb 100644 --- a/examples/src/main/scala/sttp/tapir/examples/streaming/streamingPekkoServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/streaming/streamingPekkoServer.scala @@ -1,3 +1,9 @@ +// {cat=Streaming; effects=Future; server=Pekko HTTP}: Stream response as a Pekko stream + +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.10.13 +//> using dep com.softwaremill.sttp.client3::core:3.9.7 + package sttp.tapir.examples.streaming import org.apache.pekko.actor.ActorSystem diff --git a/examples/src/main/scala/sttp/tapir/examples/testing/PekkoServerStubInterpreterExample.scala b/examples/src/main/scala/sttp/tapir/examples/testing/PekkoServerStubInterpreterExample.scala index 4d0046f2b1..68807d2e83 100644 --- a/examples/src/main/scala/sttp/tapir/examples/testing/PekkoServerStubInterpreterExample.scala +++ b/examples/src/main/scala/sttp/tapir/examples/testing/PekkoServerStubInterpreterExample.scala @@ -1,3 +1,11 @@ +// {cat=Testing; effects=Future; server=Pekko HTTP}: Test endpoints using the TapirStubInterpreter + +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-sttp-stub-server:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.10.13 +//> using dep com.softwaremill.sttp.client3::core:3.9.7 +//> using dep org.scalatest::scalatest:3.2.19 + package sttp.tapir.examples.testing import org.scalatest.flatspec.AsyncFlatSpec 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 f7faebb25f..f85cebef52 100644 --- a/examples/src/main/scala/sttp/tapir/examples/testing/SttpMockServerClientExample.scala +++ b/examples/src/main/scala/sttp/tapir/examples/testing/SttpMockServerClientExample.scala @@ -1,3 +1,12 @@ +// {cat=Testing; json=circe}: Test endpoints using the MockServer client + +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.10.13 +//> using dep com.softwaremill.sttp.tapir::sttp-mock-server:1.10.13 +//> using dep com.softwaremill.sttp.client3::core:3.9.7 +//> using dep org.mock-server:mockserver-netty:5.15.0 +//> using dep org.scalatest::scalatest:3.2.19 + package sttp.tapir.examples.testing import io.circe.generic.auto._ 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 0fa1ba49d6..53df66f5c4 100644 --- a/examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketHttp4sServer.scala @@ -1,3 +1,13 @@ +// {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.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-asyncapi-docs:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.10.13 +//> 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.7 +//> using dep org.http4s::http4s-blaze-server:0.23.16 + package sttp.tapir.examples.websocket import cats.effect.{ExitCode, IO, IOApp} @@ -32,7 +42,7 @@ object WebSocketHttp4sServer extends IOApp: // The web socket endpoint: GET /count. // We need to provide both the type & media type for the requests, and responses. Here, the requests will be // byte arrays, and responses will be returned as json. - val wsEndpoint: PublicEndpoint[Unit, Unit, Pipe[IO, String, CountResponse], Fs2Streams[IO] with WebSockets] = + val wsEndpoint: PublicEndpoint[Unit, Unit, Pipe[IO, String, CountResponse], Fs2Streams[IO] & WebSockets] = endpoint.get.in("count").out(webSocketBody[String, CodecFormat.TextPlain, CountResponse, CodecFormat.Json](Fs2Streams[IO])) // A pipe which counts the number of bytes received each second 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 6dd5ca8926..76b5194762 100644 --- a/examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketNettySyncServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketNettySyncServer.scala @@ -1,3 +1,8 @@ +// {cat=WebSocket; effects=Direct; server=Netty}: Describe and implement a WebSocket endpoint + +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.10.13 + package sttp.tapir.examples.websocket import ox.* 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 7363896264..8c1d1d8e70 100644 --- a/examples/src/main/scala/sttp/tapir/examples/websocket/webSocketPekkoServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/websocket/webSocketPekkoServer.scala @@ -1,3 +1,12 @@ +// {cat=WebSocket; effects=Future; server=Pekko HTTP}: Describe and implement a WebSocket endpoint + +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-asyncapi-docs:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.10.13 +//> using dep com.softwaremill.sttp.apispec::asyncapi-circe-yaml:0.10.0 +//> using dep com.softwaremill.sttp.client3::pekko-http-backend:3.9.7 + package sttp.tapir.examples.websocket import io.circe.generic.auto.* @@ -27,7 +36,7 @@ import scala.concurrent.{Await, Future} // The web socket endpoint: GET /ping. // We need to provide both the type & media type for the requests, and responses. Here, the requests will be // strings, and responses will be returned as json. - val wsEndpoint: PublicEndpoint[Unit, Unit, Flow[String, Response, Any], PekkoStreams with WebSockets] = + val wsEndpoint: PublicEndpoint[Unit, Unit, Flow[String, Response, Any], PekkoStreams & WebSockets] = endpoint.get.in("ping").out(webSocketBody[String, CodecFormat.TextPlain, Response, CodecFormat.Json](PekkoStreams)) implicit val actorSystem: ActorSystem = ActorSystem() @@ -70,4 +79,4 @@ import scala.concurrent.{Await, Future} .map(_ => binding) } - Await.result(bindAndCheck.flatMap(_.terminate(1.minute)).flatMap(_ => actorSystem.terminate()), 1.minute) + val _ = Await.result(bindAndCheck.flatMap(_.terminate(1.minute)).flatMap(_ => actorSystem.terminate()), 1.minute) diff --git a/generated-doc/out/_static/css/custom.css b/generated-doc/out/_static/css/custom.css new file mode 100644 index 0000000000..46ec85acca --- /dev/null +++ b/generated-doc/out/_static/css/custom.css @@ -0,0 +1,42 @@ +/* general style for all example tags */ +.example-tag { + border-width: 1px; + border-radius: 9999px; + border-style: solid; + padding-left: 0.5rem; + padding-right: 0.5rem; + margin-right: 0.25rem; + margin-top: 0.25rem; + margin-bottom: 0.25rem; +} + +/* different colors for specific tags */ +.example-effects { + color: rgb(193 21 116); + background-color: rgb(253 242 250); + border-color: rgb(252 206 238); +} + +.example-json { + color: rgb(185 56 21); + background-color: rgb(254 246 238); + border-color: rgb(249 219 175); +} + +.example-server { + color: rgb(6 118 71); + background-color: rgb(236 253 243); + border-color: rgb(169 239 197); +} + +.example-docs { + color: rgb(52 64 84); + background-color: rgb(249 250 251); + border-color: rgb(234 236 240); +} + +.example-client { + color: rgb(6 89 134); + background-color: rgb(240 249 255); + border-color: rgb(185 230 254); +} diff --git a/generated-doc/out/adopters.md b/generated-doc/out/adopters.md new file mode 100644 index 0000000000..6b86241a9a --- /dev/null +++ b/generated-doc/out/adopters.md @@ -0,0 +1,43 @@ +# Adopters + +Is your company already using tapir? We're continually expanding the "adopters" section in the documentation; the more the merrier! It would be great to feature your company's logo, but in order to do that, we'll need written permission to avoid any legal misunderstandings. + +Please email us at [tapir@softwaremill.com](mailto:tapir@softwaremill.com) from your company's email with a link to your logo (if we can use it, of course!) or with details who to kindly ask for permission to feature the logo in tapir's documentation. We'll handle the rest. + +Thank you! + +
+Adobe +Swisscom +Swissborg +
+
+Kaizo +Process Street +Tranzzo +
+
+Kelkoo group +SoftwareMill +Carvana +
+
+Moneyfarm +Ocado +Wegtam +
+
+Broad +Kensu +Colisweb +
+
+iceo +dpg +hunters +
+
+moia +pits +hootsuite +
diff --git a/generated-doc/out/client/http4s.md b/generated-doc/out/client/http4s.md index ad1ae61791..b9fc6f6ccc 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-http4s-client" % "1.10.13" ``` 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 ba23b48d1c..573dc067b9 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-play-client" % "1.10.13" ``` For **Play 2.9**, add ```scala -"com.softwaremill.sttp.tapir" %% "tapir-play29-client" % "1.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-play29-client" % "1.10.13" ``` instead. Furthermore, replace all uses of `sttp.capabilities.pekko.PekkoStreams` in the following code snippets with `sttp.capabilities.akka.AkkaStreams`. @@ -51,7 +51,7 @@ After providing the input parameters, the two following are returned: Example: ```scala -import sttp.tapir._ +import sttp.tapir.* import sttp.tapir.client.play.PlayClientInterpreter import sttp.capabilities.pekko.PekkoStreams @@ -60,7 +60,7 @@ import scala.concurrent.Future import play.api.libs.ws.StandaloneWSClient -def example[I, E, O, R >: PekkoStreams](implicit wsClient: StandaloneWSClient) { +def example[I, E, O, R >: PekkoStreams](implicit wsClient: StandaloneWSClient): Unit = val e: PublicEndpoint[I, E, O, R] = ??? val inputArgs: I = ??? @@ -71,7 +71,6 @@ def example[I, E, O, R >: PekkoStreams](implicit wsClient: StandaloneWSClient) { val result: Future[Either[E, O]] = req .execute() .map(responseParser) -} ``` ## Limitations diff --git a/generated-doc/out/client/sttp.md b/generated-doc/out/client/sttp.md index 42eb9cd895..8ed4330ed8 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-sttp-client" % "1.10.13" ``` To make requests using an endpoint definition using the [sttp client](https://github.com/softwaremill/sttp), import: @@ -66,10 +66,10 @@ convert sttp's `WebSocket` instance into a pipe. This logic is looked up via the The required imports are as follows: ```scala -import sttp.tapir.client.sttp.ws.pekkohttp._ // for pekko-streams -import sttp.tapir.client.sttp.ws.akkahttp._ // for akka-streams -import sttp.tapir.client.sttp.ws.fs2._ // for fs2 -import sttp.tapir.client.sttp.ws.zio._ // for zio +import sttp.tapir.client.sttp.ws.pekkohttp.* // for pekko-streams +import sttp.tapir.client.sttp.ws.akkahttp.* // for akka-streams +import sttp.tapir.client.sttp.ws.fs2.* // for fs2 +import sttp.tapir.client.sttp.ws.zio.* // for zio ``` No additional dependencies are needed, as both of the above implementations are included in the main interpreter, @@ -85,9 +85,9 @@ If you'd like to skip that step, e.g. when testing redirects, it's possible to o description, for example: ```scala :compile-only -import sttp.tapir._ +import sttp.tapir.* import sttp.tapir.client.sttp.SttpClientInterpreter -import sttp.client3._ +import sttp.client3.* SttpClientInterpreter() .toRequest(endpoint.get.in("hello").in(query[String]("name")), Some(uri"http://localhost:8080")) @@ -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.10.12" +"com.softwaremill.sttp.tapir" %%% "tapir-sttp-client" % "1.10.13" "io.github.cquiroz" %%% "scala-java-time" % "2.2.0" // implementations of java.time classes for Scala.JS ``` diff --git a/generated-doc/out/conf.py b/generated-doc/out/conf.py index 5afa049774..d7df20c3ab 100644 --- a/generated-doc/out/conf.py +++ b/generated-doc/out/conf.py @@ -101,6 +101,12 @@ # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] +# These paths are either relative to html_static_path +# or fully qualified paths (eg. https://...) +html_css_files = [ + 'css/custom.css', +] + # Custom sidebar templates, must be a dictionary that maps document names # to template names. # diff --git a/generated-doc/out/docs/asyncapi.md b/generated-doc/out/docs/asyncapi.md index 8eacc0d08b..c41c0c9f49 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-asyncapi-docs" % "1.10.13" "com.softwaremill.sttp.apispec" %% "asyncapi-circe-yaml" % "..." // see https://github.com/softwaremill/sttp-apispec ``` @@ -15,16 +15,16 @@ object: ```scala import sttp.apispec.asyncapi.AsyncAPI -import sttp.capabilities.akka.AkkaStreams -import sttp.tapir._ +import sttp.capabilities.pekko.PekkoStreams +import sttp.tapir.* import sttp.tapir.docs.asyncapi.AsyncAPIInterpreter -import sttp.tapir.generic.auto._ -import sttp.tapir.json.circe._ -import io.circe.generic.auto._ +import sttp.tapir.generic.auto.* +import sttp.tapir.json.circe.* +import io.circe.generic.auto.* case class Response(msg: String, count: Int) val echoWS = endpoint.out( - webSocketBody[String, CodecFormat.TextPlain, Response, CodecFormat.Json](AkkaStreams)) + webSocketBody[String, CodecFormat.TextPlain, Response, CodecFormat.Json](PekkoStreams)) val docs: AsyncAPI = AsyncAPIInterpreter().toAsyncAPI(echoWS, "Echo web socket", "1.0") ``` @@ -56,7 +56,7 @@ Multiple endpoints can be converted to an `AsyncAPI` instance by calling the met The asyncapi case classes can then be serialised, either to JSON or YAML using [Circe](https://circe.github.io/circe/): ```scala -import sttp.apispec.asyncapi.circe.yaml._ +import sttp.apispec.asyncapi.circe.yaml.* println(docs.toYaml) ``` @@ -84,7 +84,7 @@ Specification extensions can be added by first importing an extension method, an method which manipulates the appropriate attribute on the schema, endpoint or endpoint input/output: ```scala -import sttp.tapir.docs.apispec.DocsExtensionAttribute._ +import sttp.tapir.docs.apispec.DocsExtensionAttribute.* endpoint .post @@ -97,4 +97,4 @@ look at **OpenAPI Specification Extensions** section of [documentation](../docs/ ## Exposing AsyncAPI documentation -AsyncAPI documentation can be exposed through the [AsyncAPI playground](https://playground.asyncapi.io). \ No newline at end of file +AsyncAPI documentation can be exposed through the [AsyncAPI playground](https://playground.asyncapi.io). diff --git a/generated-doc/out/docs/json-schema.md b/generated-doc/out/docs/json-schema.md index 51165a54cb..ebc77bae9f 100644 --- a/generated-doc/out/docs/json-schema.md +++ b/generated-doc/out/docs/json-schema.md @@ -3,29 +3,29 @@ 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-apispec-docs" % "1.10.13" ``` Schema generation can now be performed like in the following example: ```scala import sttp.apispec.{Schema => ASchema} -import sttp.tapir._ -import sttp.tapir.docs.apispec.schema._ -import sttp.tapir.generic.auto._ +import sttp.tapir.* +import sttp.tapir.docs.apispec.schema.* +import sttp.tapir.generic.auto.* - object Childhood { - case class Child(age: Int, height: Option[Int]) - } - case class Parent(innerChildField: Child, childDetails: Childhood.Child) - case class Child(childName: String) // to illustrate unique name generation - val tSchema = implicitly[Schema[Parent]] - - val jsonSchema: ASchema = TapirSchemaToJsonSchema( - tSchema, - markOptionsAsNullable = true, - metaSchema = MetaSchemaDraft04 // default - // schemaName = sttp.atpir.docs.apispec.defaultSchemaName // default +object Childhood { + case class Child(age: Int, height: Option[Int]) +} +case class Parent(innerChildField: Child, childDetails: Childhood.Child) +case class Child(childName: String) // to illustrate unique name generation +val tSchema = implicitly[Schema[Parent]] + +val jsonSchema: ASchema = TapirSchemaToJsonSchema( + tSchema, + markOptionsAsNullable = true, + metaSchema = MetaSchemaDraft04 // default + // schemaName = sttp.atpir.docs.apispec.defaultSchemaName // default ) ``` @@ -42,28 +42,28 @@ you will get a codec for `sttp.apispec.Schema`: ```scala import io.circe.Printer -import io.circe.syntax._ -import sttp.apispec.circe._ -import sttp.apispec.{Schema => ASchema, SchemaType => ASchemaType} -import sttp.tapir._ -import sttp.tapir.docs.apispec.schema._ -import sttp.tapir.generic.auto._ +import io.circe.syntax.* +import sttp.apispec.circe.* +import sttp.apispec.{Schema => ASchema} +import sttp.tapir.* +import sttp.tapir.docs.apispec.schema.* +import sttp.tapir.generic.auto.* import sttp.tapir.Schema.annotations.title - object Childhood { - @title("my child") case class Child(age: Int, height: Option[Int]) - } - case class Parent(innerChildField: Child, childDetails: Childhood.Child) - case class Child(childName: String) - val tSchema = implicitly[Schema[Parent]] - - val jsonSchema: ASchema = TapirSchemaToJsonSchema( - tSchema, - markOptionsAsNullable = true) - - // JSON serialization - val schemaAsJson = jsonSchema.asJson - val schemaStr: String = Printer.spaces2.print(schemaAsJson.deepDropNullValues) +object Childhood { + @title("my child") case class Child(age: Int, height: Option[Int]) +} +case class Parent(innerChildField: Child, childDetails: Childhood.Child) +case class Child(childName: String) +val tSchema = implicitly[Schema[Parent]] + +val jsonSchema: ASchema = TapirSchemaToJsonSchema( + tSchema, + markOptionsAsNullable = true) + +// JSON serialization +val schemaAsJson = jsonSchema.asJson +val schemaStr: String = Printer.spaces2.print(schemaAsJson.deepDropNullValues) ``` The title annotation of the object will be by default the name of the case class. You can customize it with `@title` annotation. diff --git a/generated-doc/out/docs/openapi.md b/generated-doc/out/docs/openapi.md index 59e1b38fbd..1ffce71124 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-bundle" % "1.10.13" ``` Then, you can interpret a list of endpoints using `SwaggerInterpreter`. The result will be a list of file-serving @@ -22,7 +22,7 @@ with the endpoints for which the documentation is generated, will need in turn t interpreter. For example: ```scala -import sttp.tapir._ +import sttp.tapir.* import sttp.tapir.swagger.bundle.SwaggerInterpreter import sttp.tapir.server.netty.{NettyFutureServerInterpreter, FutureRoute} @@ -55,7 +55,7 @@ for details. Similarly as above, you'll need the following dependency: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-redoc-bundle" % "1.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-redoc-bundle" % "1.10.13" ``` 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-openapi-docs" % "1.10.13" "com.softwaremill.sttp.apispec" %% "openapi-circe-yaml" % "..." // see https://github.com/softwaremill/sttp-apispec ``` @@ -77,7 +77,7 @@ object: ```scala import sttp.apispec.openapi.OpenAPI -import sttp.tapir._ +import sttp.tapir.* import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter val booksListing = endpoint.in(path[String]("bookId")) @@ -111,7 +111,7 @@ OpenAPIDocsInterpreter().toOpenAPI(List(addBook, booksListing, booksListingByGen The openapi case classes can then be serialised to YAML using [Circe](https://circe.github.io/circe/): ```scala -import sttp.apispec.openapi.circe.yaml._ +import sttp.apispec.openapi.circe.yaml.* println(docs.toYaml) ``` @@ -120,8 +120,8 @@ Or to JSON: ```scala import io.circe.Printer -import io.circe.syntax._ -import sttp.apispec.openapi.circe._ +import io.circe.syntax.* +import sttp.apispec.openapi.circe.* println(Printer.spaces2.print(docs.asJson)) ``` @@ -133,15 +133,15 @@ 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-openapi-docs" % "1.10.13" "com.softwaremill.sttp.apispec" %% "openapi-circe-yaml" % "..." // see https://github.com/softwaremill/sttp-apispec ``` and generate the documentation by importing valid extension methods and explicitly specifying the "3.0.3" version in the OpenAPI model: ```scala import sttp.apispec.openapi.OpenAPI -import sttp.apispec.openapi.circe.yaml._ // for `toYaml` extension method -import sttp.tapir._ +import sttp.apispec.openapi.circe.yaml.* // for `toYaml` extension method +import sttp.tapir.* import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter case class Book(id: Option[Long], title: Option[String]) @@ -163,19 +163,19 @@ 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-swagger-ui" % "1.10.13" ``` or `tapir-redoc`: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-redoc" % "1.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-redoc" % "1.10.13" ``` Then, you'll need to pass the server endpoints to your server interpreter. For example, using akka-http: ```scala -import sttp.apispec.openapi.circe.yaml._ -import sttp.tapir._ +import sttp.apispec.openapi.circe.yaml.* +import sttp.tapir.* import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter import sttp.tapir.server.netty.{NettyFutureServerInterpreter, FutureRoute} import sttp.tapir.swagger.SwaggerUI @@ -223,7 +223,7 @@ that is they will form a single security requirement, with multiple schemes, e.g ```scala import sttp.model.headers.WWWAuthenticateChallenge -import sttp.tapir._ +import sttp.tapir.* val multiAuthEndpoint = endpoint.post @@ -247,7 +247,7 @@ can be done in the security logic, server logic, or by mapping the inputs using ```scala import sttp.model.headers.WWWAuthenticateChallenge -import sttp.tapir._ +import sttp.tapir.* val alternativeAuthEndpoint = endpoint.securityIn( // auth.apiKey(...).and(auth.apiKey(..)) will map the request headers to a tuple (Option[String], Option[String]) @@ -281,16 +281,16 @@ Specification extensions can be added by first importing an extension method, an method which manipulates the appropriate attribute on the schema, endpoint or endpoint input/output: ```scala -import sttp.apispec.openapi._ -import sttp.apispec.openapi.circe._ -import sttp.apispec.openapi.circe.yaml._ -import sttp.tapir._ -import sttp.tapir.json.circe._ -import sttp.tapir.generic.auto._ -import io.circe.generic.auto._ +import sttp.apispec.openapi.* +import sttp.apispec.openapi.circe.* +import sttp.apispec.openapi.circe.yaml.* +import sttp.tapir.* +import sttp.tapir.json.circe.* +import sttp.tapir.generic.auto.* +import io.circe.generic.auto.* import sttp.tapir.docs.apispec.DocsExtension -import sttp.tapir.docs.apispec.DocsExtensionAttribute._ +import sttp.tapir.docs.apispec.DocsExtensionAttribute.* import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter case class FruitAmount(fruit: String, amount: Int) @@ -329,7 +329,7 @@ If you are using `tapir-swagger-ui` you need to set `withShowExtensions` option It's possible to hide an input/output from the OpenAPI description using following syntax: ```scala -import sttp.tapir._ +import sttp.tapir.* val acceptHeader: EndpointInput[String] = header[String]("Accept").schema(_.hidden(true)) ``` diff --git a/generated-doc/out/endpoint/basics.md b/generated-doc/out/endpoint/basics.md index d1e9038f30..45e7c9252c 100644 --- a/generated-doc/out/endpoint/basics.md +++ b/generated-doc/out/endpoint/basics.md @@ -17,7 +17,7 @@ Input/output parameters (`A`, `I`, `E` and `O`) can be: Hence, an empty, initial endpoint, with no inputs and no outputs, from which all other endpoints are derived has the type: ```scala -import sttp.tapir._ +import sttp.tapir.* val endpoint: Endpoint[Unit, Unit, Unit, Unit, Any] = ??? ``` @@ -25,7 +25,7 @@ val endpoint: Endpoint[Unit, Unit, Unit, Unit, Any] = ??? For endpoints which have no security inputs, a type alias is provided which fixes `A` to `Unit`: ```scala -import sttp.tapir._ +import sttp.tapir.* type PublicEndpoint[I, E, O, -R] = Endpoint[Unit, I, E, O, R] ``` @@ -35,7 +35,7 @@ completion returns a `User`, would have the type: ```scala -import sttp.tapir._ +import sttp.tapir.* val userEndpoint: PublicEndpoint[(UUID, Int), String, User, Any] = ??? ``` diff --git a/generated-doc/out/endpoint/contenttype.md b/generated-doc/out/endpoint/contenttype.md index d1c120f109..541d1dea8f 100644 --- a/generated-doc/out/endpoint/contenttype.md +++ b/generated-doc/out/endpoint/contenttype.md @@ -24,13 +24,13 @@ On the client side, the appropriate mapping will be chosen basing on the `Conten For example: ```scala -import sttp.tapir._ +import sttp.tapir.* import sttp.tapir.Codec.{JsonCodec, XmlCodec} import sttp.model.StatusCode case class Entity(name: String) -implicit val jsonCodecForOrganization: JsonCodec[Entity] = ??? -implicit val xmlCodecForOrganization: XmlCodec[Entity] = ??? +given JsonCodec[Entity] = ??? +given XmlCodec[Entity] = ??? endpoint.out( oneOf( diff --git a/generated-doc/out/endpoint/customtypes.md b/generated-doc/out/endpoint/customtypes.md index 1449a1b01d..2e91ae14c5 100644 --- a/generated-doc/out/endpoint/customtypes.md +++ b/generated-doc/out/endpoint/customtypes.md @@ -1,4 +1,4 @@ -# Custom types +# Adding support for custom types To support a custom type, you'll need to provide an implicit `Codec` for that type, or the components to create such a codec. @@ -46,29 +46,26 @@ need to provide two mappings: For example, to support a custom id type: ```scala -import scala.util._ +import scala.util.* -class MyId private (id: String) { +class MyId private (id: String): override def toString(): String = id -} -object MyId { - def parse(id: String): Try[MyId] = { - Success(new MyId(id)) - } -} + +object MyId: + def parse(id: String): Try[MyId] = Success(new MyId(id)) ``` ```scala -import sttp.tapir._ +import sttp.tapir.* import sttp.tapir.CodecFormat.TextPlain -def decode(s: String): DecodeResult[MyId] = MyId.parse(s) match { +def decode(s: String): DecodeResult[MyId] = MyId.parse(s) match case Success(v) => DecodeResult.Value(v) case Failure(f) => DecodeResult.Error(s, f) -} + def encode(id: MyId): String = id.toString -implicit val myIdCodec: Codec[String, MyId, TextPlain] = +given Codec[String, MyId, TextPlain] = Codec.string.mapDecode(decode)(encode) ``` @@ -77,7 +74,7 @@ Or, using the type alias for codecs in the `TextPlain` format and `String` as th ```scala import sttp.tapir.Codec.PlainCodec -implicit val myIdCodec: PlainCodec[MyId] = Codec.string.mapDecode(decode)(encode) +given PlainCodec[MyId] = Codec.string.mapDecode(decode)(encode) ``` ```{note} diff --git a/generated-doc/out/endpoint/enumerations.md b/generated-doc/out/endpoint/enumerations.md index 47e6f2700e..20f1a438f3 100644 --- a/generated-doc/out/endpoint/enumerations.md +++ b/generated-doc/out/endpoint/enumerations.md @@ -30,15 +30,14 @@ assumes that the low-level representation of the enumeration is a string. Encodi decoding performs a case-insensitive search through the enumeration's values. For example: ```scala -import sttp.tapir._ +import sttp.tapir.* -object Features extends Enumeration { +object Features extends Enumeration: type Feature = Value val A: Feature = Value("a") val B: Feature = Value("b") val C: Feature = Value("c") -} query[Features.Feature]("feature") ``` @@ -78,17 +77,16 @@ would be considered by the compiler. For example: ```scala -import sttp.tapir._ +import sttp.tapir.* import sttp.tapir.Codec.PlainCodec sealed trait Feature -object Feature { +object Feature: case object A extends Feature case object B extends Feature case object C extends Feature -} -implicit val featureCodec: PlainCodec[Feature] = +given PlainCodec[Feature] = Codec.derivedEnumeration[String, Feature].defaultStringBased query[Feature]("feature") @@ -99,14 +97,14 @@ default `Enumeration` codec (using `.toString`). Such a codec can be similarly c and `decode` functions as parameters to the value returned to `derivedEnumeration`: ```scala -import sttp.tapir._ +import sttp.tapir.* import sttp.tapir.Codec.PlainCodec sealed trait Color case object Blue extends Color case object Red extends Color -implicit val colorCodec: PlainCodec[Color] = { +given PlainCodec[Color] = Codec.derivedEnumeration[String, Color]( (_: String) match { case "red" => Some(Red) @@ -115,7 +113,6 @@ implicit val colorCodec: PlainCodec[Color] = { }, _.toString.toLowerCase ) -} ``` ### Creating an enum codec by hand @@ -131,16 +128,15 @@ If an input/output contains multiple enumeration values, delimited e.g. using a type is a simple wrapper for a list of `T`-values. For example, if the query parameter is required: ```scala -import sttp.tapir._ +import sttp.tapir.* import sttp.tapir.model.CommaSeparated -object Features extends Enumeration { +object Features extends Enumeration: type Feature = Value val A: Feature = Value("a") val B: Feature = Value("b") val C: Feature = Value("c") -} query[CommaSeparated[Features.Feature]]("features") ``` @@ -173,25 +169,24 @@ represent the enumeration's values in the documentation). For example, to use an the circe library for JSON parsing/serialisation, and automatic schema derivation for case classes: ```scala -import io.circe._ -import io.circe.generic.auto._ -import sttp.tapir._ -import sttp.tapir.json.circe._ -import sttp.tapir.generic.auto._ +import io.circe.* +import io.circe.generic.auto.* +import sttp.tapir.* +import sttp.tapir.json.circe.* +import sttp.tapir.generic.auto.* -object Features extends Enumeration { +object Features extends Enumeration: type Feature = Value val A: Feature = Value("a") val B: Feature = Value("b") val C: Feature = Value("c") -} case class Body(someField: String, feature: Features.Feature) -// these need to be provided so that circe knows how to encode/decode enumerations -implicit val enumDecoder: Decoder[Features.Feature] = Decoder.decodeEnumeration(Features) -implicit val enumEncoder: Encoder[Features.Feature] = Encoder.encodeEnumeration(Features) +// these need to be provided so that circe knows how to encode/decode enumerations - will work only in Scala2! +given Decoder[Features.Feature] = Decoder.decodeEnumeration(Features) +given Encoder[Features.Feature] = Encoder.encodeEnumeration(Features) // the schema for the body is automatically-derived, using the default schema for // enumerations (Schema.derivedEnumerationValue) @@ -203,17 +198,16 @@ enumeration is an integer), using `Schema.derivedEnumerationValueCustomise.apply to provide the schema an implicit/given value: ```scala -import sttp.tapir._ +import sttp.tapir.* -object Features extends Enumeration { +object Features extends Enumeration: type Feature = Value val A: Feature = Value("a") val B: Feature = Value("b") val C: Feature = Value("c") -} -implicit val customFeatureSchema: Schema[Features.Feature] = +given Schema[Features.Feature] = Schema.derivedEnumerationValueCustomise[Features.Feature]( encode = Some { case Features.A => 0 @@ -239,28 +233,26 @@ need to be created using `.derivedEnumeration`, instead of the more general `.de For example: ```scala -import sttp.tapir._ +import sttp.tapir.* sealed trait Feature -object Feature { +object Feature: case object A extends Feature case object B extends Feature case object C extends Feature -} -implicit val featureSchema: Schema[Feature] = +given Schema[Feature] = Schema.derivedEnumeration[Feature].defaultStringBased ``` Similarly, using Scala 3's enums: ```scala -enum ColorEnum { +enum ColorEnum: case Green extends ColorEnum case Pink extends ColorEnum -} -given Schema[ColorEnum] = Schema.derivedEnumeration.defaultStringBased +given Schema.derivedEnumeration.defaultStringBased ``` ### Scala 3 string-based constant union types to enum @@ -282,7 +274,7 @@ case class Foo(aOrB: "a" | "b", optA: Option["a"]) derives Schema ### Creating an enum schema by hand -Creating an enumeration [schema](schema.md) by hand is exactly the same as for any other type. The only difference +Creating an enumeration [schema](schemas.md) by hand is exactly the same as for any other type. The only difference is that an enumeration [validator](validation.md) has to be added to the schema. ## Next diff --git a/generated-doc/out/endpoint/forms.md b/generated-doc/out/endpoint/forms.md index b42b3d56ee..14ebe60cce 100644 --- a/generated-doc/out/endpoint/forms.md +++ b/generated-doc/out/endpoint/forms.md @@ -6,7 +6,7 @@ An URL-encoded form input/output can be specified in two ways. First, it is poss `Seq[(String, String)]`, or `Map[String, String]` (which is more convenient if fields can't have multiple values): ```scala -import sttp.tapir._ +import sttp.tapir.* formBody[Seq[(String, String)]]: EndpointIO.Body[String, Seq[(String, String)]] formBody[Map[String, String]]: EndpointIO.Body[String, Map[String, String]] @@ -16,8 +16,8 @@ Second, form data can be mapped to a case class. The codec for the case class is compile-time. The fields of the case class should have types, for which there is a plain text codec. For example: ```scala -import sttp.tapir._ -import sttp.tapir.generic.auto._ +import sttp.tapir.* +import sttp.tapir.generic.auto.* case class RegistrationForm(name: String, age: Int, news: Boolean, city: Option[String]) @@ -33,7 +33,7 @@ Similarly as above, multipart form input/outputs can be specified in two ways. T use: ```scala -import sttp.tapir._ +import sttp.tapir.* import sttp.model.Part multipartBody: EndpointIO.Body[Seq[RawPart], Seq[Part[Array[Byte]]]] @@ -56,10 +56,10 @@ Additionally, the case class to which the multipart body is mapped can contain b For example: ```scala -import sttp.tapir._ +import sttp.tapir.* import sttp.model.Part import java.io.File -import sttp.tapir.generic.auto._ +import sttp.tapir.generic.auto.* case class RegistrationForm(userData: User, photo: Part[File], news: Boolean) case class User(email: String) @@ -72,10 +72,10 @@ If there can be none or multiple parts for the same name, the fields can be wrap or any other container `C` for which exists a codec `List[T] => C[T]` ```scala -import sttp.tapir._ +import sttp.tapir.* import sttp.model.Part import java.io.File -import sttp.tapir.generic.auto._ +import sttp.tapir.generic.auto.* case class RegistrationForm(userData: Option[User], photos: List[Part[File]], news: Option[Part[Boolean]]) case class User(email: String) diff --git a/generated-doc/out/endpoint/integrations.md b/generated-doc/out/endpoint/integrations.md index 5ad0bd039d..67341723f6 100644 --- a/generated-doc/out/endpoint/integrations.md +++ b/generated-doc/out/endpoint/integrations.md @@ -1,4 +1,4 @@ -# Datatypes integrations +# Third-party datatype libraries integrations ```{note} Note that the codecs defined by the tapir integrations are used only when the specific types (e.g. enumerations) are @@ -12,17 +12,17 @@ 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-cats" % "1.10.13" ``` -- `import sttp.tapir.integ.cats.codec._` - brings schema, validator and codec instances -- `import sttp.tapir.integ.cats.syntax._` - brings additional syntax for `tapir` types +- `import sttp.tapir.integ.cats.codec.*` - brings schema, validator and codec instances +- `import sttp.tapir.integ.cats.syntax.*` - brings additional syntax for `tapir` types Additionally, the `tapir-cats-effect` module contains an implementation of the `CatsMonadError` class, providing a bridge between the sttp-internal `MonadError` and the cats-effect `Sync` typeclass: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-cats-effect" % "1.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-cats-effect" % "1.10.13" ``` ## Refined integration @@ -31,11 +31,11 @@ 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-refined" % "1.10.13" ``` You'll need to extend the `sttp.tapir.codec.refined.TapirCodecRefined` -trait or `import sttp.tapir.codec.refined._` to bring the implicit values into scope. +trait or `import sttp.tapir.codec.refined.*` to bring the implicit values into scope. The refined codecs contain a validator which wrap/unwrap the value from/to its refined equivalent. @@ -52,13 +52,13 @@ 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-iron" % "1.10.13" ``` The module is only available for Scala 3 since iron is not designed to work with Scala 2. You'll need to extend the `sttp.tapir.codec.refined.TapirCodecIron` -trait or `import sttp.tapir.codec.iron._` to bring the implicit values into scope. +trait or `import sttp.tapir.codec.iron.*` to bring the implicit values into scope. The iron codecs contain a validator which apply the constraint to validated value. @@ -86,11 +86,12 @@ Example for `circe`: ```scala case class IronException(error: String) extends Exception(error) -inline given (using inline constraint: Constraint[Int, Positive]): Decoder[Age] = summon[Decoder[Int]].map(unrefinedValue => - unrefinedValue.refineEither[Positive] match - case Right(value) => value - case Left(errorMessage) => throw IronException(s"Could not refine value $unrefinedValue: $errorMessage") -) +inline given (using inline constraint: Constraint[Int, Positive]): Decoder[Age] = + summon[Decoder[Int]].map(unrefinedValue => + unrefinedValue.refineEither[Positive] match + case Right(value) => value + case Left(errorMessage) => throw IronException(s"Could not refine value $unrefinedValue: $errorMessage") + ) ``` Then failure handler matching `IronException` is needed. Remember to create the interceptor: @@ -144,10 +145,10 @@ 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-enumeratum" % "1.10.13" ``` -Then, `import sttp.tapir.codec.enumeratum._`, or extends the `sttp.tapir.codec.enumeratum.TapirCodecEnumeratum` trait. +Then, `import sttp.tapir.codec.enumeratum.*`, or extends the `sttp.tapir.codec.enumeratum.TapirCodecEnumeratum` trait. This will bring into scope implicit values for values extending `*EnumEntry`. @@ -157,10 +158,10 @@ 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-newtype" % "1.10.13" ``` -Then, `import sttp.tapir.codec.newtype._`, or extend the `sttp.tapir.codec.newtype.TapirCodecNewType` trait to bring the implicit values into scope. +Then, `import sttp.tapir.codec.newtype.*`, or extend the `sttp.tapir.codec.newtype.TapirCodecNewType` trait to bring the implicit values into scope. ## Monix NewType integration @@ -168,10 +169,10 @@ 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-monix-newtype" % "1.10.13" ``` -Then, `import sttp.tapir.codec.monix.newtype._`, or extend the `sttp.tapir.codec.monix.newtype.TapirCodecMonixNewType` trait to bring the implicit values into scope. +Then, `import sttp.tapir.codec.monix.newtype.*`, or extend the `sttp.tapir.codec.monix.newtype.TapirCodecMonixNewType` trait to bring the implicit values into scope. ## ZIO Prelude Newtype integration @@ -179,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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-zio-prelude" % "1.10.13" ``` Then, mix in `sttp.tapir.codec.zio.prelude.newtype.TapirNewtypeSupport` into your newtype to bring the implicit values into scope: @@ -206,7 +207,7 @@ type Bar = Bar.Type // Explicitly provide the base type of your newtype when instantiating the helper, in this case, String. val BarSupport = TapirNewtype[String](Bar) -import BarSupport._ +import BarSupport.* implicitly[Schema[Bar]] implicitly[PlainCodec[Bar]] ``` @@ -218,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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-derevo" % "1.10.13" ``` Then you can derive schema for your ADT along with other typeclasses besides ADT declaration itself: @@ -234,9 +235,9 @@ case class Person(name: String, age: Int) @derive(schema("Type of currency in the country")) sealed trait Currency - object Currency {case object CommunisticCurrency extends Currency +object Currency: + case object CommunisticCurrency extends Currency case class USD(amount: Long) extends Currency -} ``` The annotation will simply generate a `Schema[T]` for your type `T` and put it into companion object. @@ -249,11 +250,10 @@ import derevo.derive import sttp.tapir.derevo.schema import io.estatico.newtype.macros.newtype -object types { +object types: @derive(schema) @newtype case class Amount(i: Int) -} ``` Resulting schema will be equivalent to `implicitly[Schema[Int]].map(i => Some(types.Amount(i)))`. diff --git a/generated-doc/out/endpoint/ios.md b/generated-doc/out/endpoint/ios.md index efd223d8a1..d55e55f1f3 100644 --- a/generated-doc/out/endpoint/ios.md +++ b/generated-doc/out/endpoint/ios.md @@ -53,10 +53,10 @@ other values in tapir, endpoint input/output descriptions are immutable. For exa parameters, `start` (mandatory) and `limit` (optional) can be written down as: ```scala -import sttp.tapir._ -import sttp.tapir.generic.auto._ -import sttp.tapir.json.circe._ -import io.circe.generic.auto._ +import sttp.tapir.* +import sttp.tapir.generic.auto.* +import sttp.tapir.json.circe.* +import io.circe.generic.auto.* import java.util.UUID case class User(name: String) @@ -75,10 +75,10 @@ parameters, but also to define template-endpoints, which can then be further spe base endpoint for our API, where all paths always start with `/api/v1.0`, and errors are always returned as a json: ```scala -import sttp.tapir._ -import sttp.tapir.generic.auto._ -import sttp.tapir.json.circe._ -import io.circe.generic.auto._ +import sttp.tapir.* +import sttp.tapir.generic.auto.* +import sttp.tapir.json.circe.* +import io.circe.generic.auto.* case class ErrorInfo(message: String) @@ -106,7 +106,7 @@ There's a couple of ways to map over an input/output. First, there's the `map[II which accepts functions which provide the mapping in both directions. For example: ```scala -import sttp.tapir._ +import sttp.tapir.* import java.util.UUID case class Paging(from: UUID, limit: Option[Int]) @@ -138,7 +138,7 @@ The `Endpoint.mapIn`, `Endpoint.mapInTo` etc. have the same signatures are the o Inputs and outputs can also be built for case classes using annotations. For example, for the case class `User` ```scala -import sttp.tapir.EndpointIO.annotations._ +import sttp.tapir.EndpointIO.annotations.* case class User( @query @@ -151,7 +151,7 @@ case class User( endpoint input can be generated using macro `EndpointInput.derived[User]` which is equivalent to ```scala -import sttp.tapir._ +import sttp.tapir.* val userInput: EndpointInput[User] = query[String]("user").and(cookie[Long]("sessionId")).mapTo[User] @@ -185,7 +185,7 @@ annotation `@header` it has optional parameter to specify alternative name for q by annotation `@endpointInput`. For example, ```scala -import sttp.tapir.EndpointIO.annotations._ +import sttp.tapir.EndpointIO.annotations.* @endpointInput("books/{year}/{genre}") case class Book( diff --git a/generated-doc/out/endpoint/json.md b/generated-doc/out/endpoint/json.md index 0186fb4e37..8391609fbe 100644 --- a/generated-doc/out/endpoint/json.md +++ b/generated-doc/out/endpoint/json.md @@ -39,8 +39,8 @@ serialising/deserialising of the body must be part of the [server logic](../serv A schema can be provided in this case as well: ```scala -import sttp.tapir._ -import sttp.tapir.generic.auto._ +import sttp.tapir.* +import sttp.tapir.generic.auto.* case class MyBody(field: Int) stringJsonBody.schema(implicitly[Schema[MyBody]].as[String]) ``` @@ -50,13 +50,13 @@ 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-json-circe" % "1.10.13" ``` -Next, import the package (or extend the `TapirJsonCirce` trait, see [MyTapir](../mytapir.md)): +Next, import the package (or extend the `TapirJsonCirce` trait, see [MyTapir](../other/mytapir.md)): ```scala -import sttp.tapir.json.circe._ +import sttp.tapir.json.circe.* ``` The above import brings into scope the `jsonBody[T]` body input/output description, which creates a codec, given an @@ -68,10 +68,10 @@ Note that when using Circe's auto derivation, any encoders/decoders for custom t For example, to automatically generate a JSON codec for a case class: ```scala -import sttp.tapir._ -import sttp.tapir.json.circe._ -import sttp.tapir.generic.auto._ -import io.circe.generic.auto._ +import sttp.tapir.* +import sttp.tapir.json.circe.* +import sttp.tapir.generic.auto.* +import io.circe.generic.auto.* case class Book(author: String, title: String, year: Int) @@ -84,7 +84,7 @@ Circe lets you select an instance of `io.circe.Printer` to configure the way JSO Tapir uses `Printer.nospaces`, which would render: ```scala -import io.circe._ +import io.circe.* Json.obj( "key1" -> Json.fromString("present"), @@ -102,14 +102,13 @@ Suppose we would instead want to omit `null`-values from the object and pretty-p overriding the `jsonPrinter` in `tapir.circe.json.TapirJsonCirce`: ```scala -import sttp.tapir.json.circe._ +import sttp.tapir.json.circe.* import io.circe.Printer -object MyTapirJsonCirce extends TapirJsonCirce { +object MyTapirJsonCirce extends TapirJsonCirce: override def jsonPrinter: Printer = Printer.spaces2.copy(dropNullValues = true) -} -import MyTapirJsonCirce._ +import MyTapirJsonCirce.* ``` Now the above JSON object will render as @@ -123,28 +122,27 @@ 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-json-upickle" % "1.10.13" ``` -Next, import the package (or extend the `TapirJsonuPickle` trait, see [MyTapir](../mytapir.md) and add `TapirJsonuPickle` not `TapirCirceJson`): +Next, import the package (or extend the `TapirJsonuPickle` trait, see [MyTapir](../other/mytapir.md) and add `TapirJsonuPickle` not `TapirCirceJson`): ```scala -import sttp.tapir.json.upickle._ +import sttp.tapir.json.upickle.* ``` µPickle requires a `ReadWriter` in scope for each type you want to serialize. In order to provide one use the `macroRW` macro in the companion object as follows: ```scala -import sttp.tapir._ -import sttp.tapir.generic.auto._ -import upickle.default._ -import sttp.tapir.json.upickle._ +import sttp.tapir.* +import sttp.tapir.generic.auto.* +import upickle.default.* +import sttp.tapir.json.upickle.* case class Book(author: String, title: String, year: Int) -object Book { - implicit val rw: ReadWriter[Book] = macroRW -} +object Book: + given ReadWriter[Book] = macroRW val bookInput: EndpointIO[Book] = jsonBody[Book] ``` @@ -158,19 +156,19 @@ 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-json-play" % "1.10.13" ``` For **Play 2.9** use: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-json-play29" % "1.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-json-play29" % "1.10.13" ``` -Next, import the package (or extend the `TapirJsonPlay` trait, see [MyTapir](../mytapir.md) and add `TapirJsonPlay` not `TapirCirceJson`): +Next, import the package (or extend the `TapirJsonPlay` trait, see [MyTapir](../other/mytapir.md) and add `TapirJsonPlay` not `TapirCirceJson`): ```scala -import sttp.tapir.json.play._ +import sttp.tapir.json.play.* ``` Play JSON requires `Reads` and `Writes` implicit values in scope for each type you want to serialize. @@ -180,13 +178,13 @@ 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-json-spray" % "1.10.13" ``` -Next, import the package (or extend the `TapirJsonSpray` trait, see [MyTapir](../mytapir.md) and add `TapirJsonSpray` not `TapirCirceJson`): +Next, import the package (or extend the `TapirJsonSpray` trait, see [MyTapir](../other/mytapir.md) and add `TapirJsonSpray` not `TapirCirceJson`): ```scala -import sttp.tapir.json.spray._ +import sttp.tapir.json.spray.* ``` Spray JSON requires a `JsonFormat` implicit value in scope for each type you want to serialize. @@ -196,13 +194,13 @@ 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-json-tethys" % "1.10.13" ``` -Next, import the package (or extend the `TapirJsonTethys` trait, see [MyTapir](../mytapir.md) and add `TapirJsonTethys` not `TapirCirceJson`): +Next, import the package (or extend the `TapirJsonTethys` trait, see [MyTapir](../other/mytapir.md) and add `TapirJsonTethys` not `TapirCirceJson`): ```scala -import sttp.tapir.json.tethysjson._ +import sttp.tapir.json.tethysjson.* ``` Tethys JSON requires `JsonReader` and `JsonWriter` implicit values in scope for each type you want to serialize. @@ -212,13 +210,13 @@ 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-jsoniter-scala" % "1.10.13" ``` -Next, import the package (or extend the `TapirJsonJsoniter` trait, see [MyTapir](../mytapir.md) and add `TapirJsonJsoniter` not `TapirCirceJson`): +Next, import the package (or extend the `TapirJsonJsoniter` trait, see [MyTapir](../other/mytapir.md) and add `TapirJsonJsoniter` not `TapirCirceJson`): ```scala -import sttp.tapir.json.jsoniter._ +import sttp.tapir.json.jsoniter.* ``` Jsoniter Scala requires `JsonValueCodec` implicit value in scope for each type you want to serialize. @@ -228,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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-json-json4s" % "1.10.13" ``` And one of the implementations: @@ -239,19 +237,19 @@ And one of the implementations: "org.json4s" %% "json4s-jackson" % "4.0.7" ``` -Next, import the package (or extend the `TapirJson4s` trait, see [MyTapir](../mytapir.md) and add `TapirJson4s` instead of `TapirCirceJson`): +Next, import the package (or extend the `TapirJson4s` trait, see [MyTapir](../other/mytapir.md) and add `TapirJson4s` instead of `TapirCirceJson`): ```scala -import sttp.tapir.json.json4s._ +import sttp.tapir.json.json4s.* ``` Json4s requires `Serialization` and `Formats` implicit values in scope, for example: ```scala -import org.json4s._ +import org.json4s.* // ... -implicit val serialization: Serialization = org.json4s.jackson.Serialization -implicit val formats: Formats = org.json4s.jackson.Serialization.formats(NoTypeHints) +given Serialization = org.json4s.jackson.Serialization +given Formats = org.json4s.jackson.Serialization.formats(NoTypeHints) ``` ## Zio JSON @@ -259,12 +257,12 @@ implicit val formats: Formats = org.json4s.jackson.Serialization.formats(NoTypeH 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-json-zio" % "1.10.13" ``` -Next, import the package (or extend the `TapirJsonZio` trait, see [MyTapir](../mytapir.md) and add `TapirJsonZio` instead of `TapirCirceJson`): +Next, import the package (or extend the `TapirJsonZio` trait, see [MyTapir](../other/mytapir.md) and add `TapirJsonZio` instead of `TapirCirceJson`): ```scala -import sttp.tapir.json.zio._ +import sttp.tapir.json.zio.* ``` Zio JSON requires `JsonEncoder` and `JsonDecoder` implicit values in scope for each type you want to serialize. @@ -274,10 +272,10 @@ Zio JSON requires `JsonEncoder` and `JsonDecoder` implicit values in scope for e You can specify query parameters in JSON format by using the `jsonQuery` method. For example, using Circe: ```scala -import sttp.tapir._ -import sttp.tapir.json.circe._ -import sttp.tapir.generic.auto._ -import io.circe.generic.auto._ +import sttp.tapir.* +import sttp.tapir.json.circe.* +import sttp.tapir.generic.auto.* +import io.circe.generic.auto.* case class Book(author: String, title: String, year: Int) diff --git a/generated-doc/out/endpoint/oneof.md b/generated-doc/out/endpoint/oneof.md index 3baa38247e..d3846c854d 100644 --- a/generated-doc/out/endpoint/oneof.md +++ b/generated-doc/out/endpoint/oneof.md @@ -42,11 +42,11 @@ For example, below is a specification for an endpoint where the error output is such a specification can then be refined and reused for other endpoints: ```scala -import sttp.tapir._ -import sttp.tapir.json.circe._ -import sttp.tapir.generic.auto._ +import sttp.tapir.* +import sttp.tapir.json.circe.* +import sttp.tapir.generic.auto.* import sttp.model.StatusCode -import io.circe.generic.auto._ +import io.circe.generic.auto.* sealed trait ErrorInfo case class NotFound(what: String) extends ErrorInfo @@ -70,11 +70,11 @@ val baseEndpoint = endpoint.errorOut( Type erasure may prevent a one-of-variant from working properly. The following example will fail at compile time because `Right[NotFound]` and `Right[BadRequest]` will become `Right[Any]`: ```scala -import sttp.tapir._ -import sttp.tapir.json.circe._ -import sttp.tapir.generic.auto._ +import sttp.tapir.* +import sttp.tapir.json.circe.* +import sttp.tapir.generic.auto.* import sttp.model.StatusCode -import io.circe.generic.auto._ +import io.circe.generic.auto.* case class ServerError(what: String) @@ -89,15 +89,18 @@ val baseEndpoint = endpoint.errorOut( oneOfVariant(StatusCode.InternalServerError, jsonBody[Left[ServerError, UserError]].description("unauthorized")), ) ) -// error: Type scala.util.Right[repl.MdocSession.MdocApp.ServerError,repl.MdocSession.MdocApp.NotFound] is not the same as its erasure. Using a runtime-class-based check it won't be possible to verify that the input matches the desired type. Use other methods to match the input to the appropriate variant instead. -// oneOfVariantValueMatcher(StatusCode.NotFound, jsonBody[Right[ServerError, NotFound]].description("not found")) { -// ^ -// error: Type scala.util.Right[repl.MdocSession.MdocApp.ServerError,repl.MdocSession.MdocApp.BadRequest] is not the same as its erasure. Using a runtime-class-based check it won't be possible to verify that the input matches the desired type. Use other methods to match the input to the appropriate variant instead. -// oneOfVariantValueMatcher(StatusCode.BadRequest, jsonBody[Right[ServerError, BadRequest]].description("unauthorized")) { -// ^ -// error: Type scala.util.Left[repl.MdocSession.MdocApp.ServerError,repl.MdocSession.MdocApp.UserError] is not the same as its erasure. Using a runtime-class-based check it won't be possible to verify that the input matches the desired type. Use other methods to match the input to the appropriate variant instead. -// oneOfVariantValueMatcher(StatusCode.InternalServerError, jsonBody[Left[ServerError, UserError]].description("unauthorized")) { -// ^ +// error: +// Type scala.util.Right[repl.MdocSession.MdocApp.ServerError, repl.MdocSession.MdocApp.NotFound], AppliedType(TypeRef(ThisType(TypeRef(NoPrefix,module class util)),class Right),List(TypeRef(ThisType(TypeRef(ThisType(TypeRef(ThisType(TypeRef(NoPrefix,module class repl)),module class MdocSession$)),module class MdocApp$)),class ServerError), TypeRef(ThisType(TypeRef(ThisType(TypeRef(ThisType(TypeRef(NoPrefix,module class repl)),module class MdocSession$)),module class MdocApp$)),class NotFound))) is not the same as its erasure. Using a runtime-class-based check it won't be possible to verify that the input matches the desired type. Use other methods to match the input to the appropriate variant instead. +// oneOfVariant(StatusCode.NotFound, jsonBody[Right[ServerError, NotFound]].description("not found")), +// ^ +// error: +// Type scala.util.Right[repl.MdocSession.MdocApp.ServerError, repl.MdocSession.MdocApp.BadRequest], AppliedType(TypeRef(ThisType(TypeRef(NoPrefix,module class util)),class Right),List(TypeRef(ThisType(TypeRef(ThisType(TypeRef(ThisType(TypeRef(NoPrefix,module class repl)),module class MdocSession$)),module class MdocApp$)),class ServerError), TypeRef(ThisType(TypeRef(ThisType(TypeRef(ThisType(TypeRef(NoPrefix,module class repl)),module class MdocSession$)),module class MdocApp$)),class BadRequest))) is not the same as its erasure. Using a runtime-class-based check it won't be possible to verify that the input matches the desired type. Use other methods to match the input to the appropriate variant instead. +// oneOfVariant(StatusCode.BadRequest, jsonBody[Right[ServerError, BadRequest]].description("unauthorized")), +// ^ +// error: +// Type scala.util.Left[repl.MdocSession.MdocApp.ServerError, repl.MdocSession.MdocApp.UserError], AppliedType(TypeRef(ThisType(TypeRef(NoPrefix,module class util)),class Left),List(TypeRef(ThisType(TypeRef(ThisType(TypeRef(ThisType(TypeRef(NoPrefix,module class repl)),module class MdocSession$)),module class MdocApp$)),class ServerError), TypeRef(ThisType(TypeRef(ThisType(TypeRef(ThisType(TypeRef(NoPrefix,module class repl)),module class MdocSession$)),module class MdocApp$)),trait UserError))) is not the same as its erasure. Using a runtime-class-based check it won't be possible to verify that the input matches the desired type. Use other methods to match the input to the appropriate variant instead. +// oneOfVariant(StatusCode.InternalServerError, jsonBody[Left[ServerError, UserError]].description("unauthorized")), +// ^ ``` The solution is therefore to handwrite a function checking that a value (of type `Any`) is of the correct type: @@ -157,7 +160,7 @@ The `.errorOutVariantPrepend` function allows prepending an error out variant, l a default. This is useful e.g. when providing a more specific error output, than the current one. For example: ```scala -import sttp.tapir._ +import sttp.tapir.* trait DomainException { def help: String @@ -190,10 +193,10 @@ Each body variant should represent the same content, and hence have the same hig To describe a body, which can be given as json, xml or plain text, create the following input/output description: ```scala -import io.circe.generic.auto._ -import sttp.tapir._ -import sttp.tapir.generic.auto._ -import sttp.tapir.json.circe._ +import io.circe.generic.auto.* +import sttp.tapir.* +import sttp.tapir.generic.auto.* +import sttp.tapir.json.circe.* case class User(name: String) diff --git a/generated-doc/out/endpoint/pickler.md b/generated-doc/out/endpoint/pickler.md index 28b9f53c2a..8d83ad940c 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-json-pickler" % "1.10.13" ``` Please note that it is available only for Scala 3 and Scala.JS 3. @@ -20,6 +20,7 @@ A pickler can be derived directly using `Pickler.derived[T]`. This will derive b ```scala import sttp.tapir.json.pickler.* +import sttp.tapir.Codec.JsonCodec case class Book(author: String, title: String, year: Int) @@ -31,7 +32,7 @@ val bookJsonStr = // { "author": "Herman Melville", "title": Moby Dick", "year": A `given` pickler in scope makes it available for `jsonQuery`, `jsonBody` and `jsonBodyWithRaw`, which need to be imported from the `sttp.tapir.json.pickler` package. For example: -```scala +```scala import sttp.tapir.* import sttp.tapir.json.pickler.* @@ -48,11 +49,11 @@ val addBook: PublicEndpoint[Book, Unit, Unit, Any] = A pickler also be derived using the `derives` keyword directly on a class: -```scala +```scala import sttp.tapir.json.pickler.* case class Book(author: String, title: String, year: Int) derives Pickler -val pickler: Pickler[Book] = summon[Pickler] +val pickler: Pickler[Book] = summon[Pickler[Book]] ``` Picklers for primitive types are available out-of-the-box. For more complex hierarchies, like nested `case class` structures or `enum`s, you'll need to provide picklers for all children (fields, enum cases etc.). Alternatively, you can use automatic derivation described below. @@ -61,7 +62,7 @@ Picklers for primitive types are available out-of-the-box. For more complex hier Picklers can be derived at usage side, when required, by adding the auto-derivation import: -```scala +```scala import sttp.tapir.json.pickler.* import sttp.tapir.json.pickler.generic.auto.* @@ -81,8 +82,8 @@ However, this can negatively impact compilation performance, as the same pickler It is possible to configure schema and codec derivation by providing an implicit `sttp.tapir.pickler.PicklerConfiguration`. This configuration allows switching field naming policy to `snake_case`, `kebab_case`, or an arbitrary transformation function, as well as setting the field name/value for the coproduct (sealed hierarchy) type discriminator, which is discussed in details in further sections. -```scala -import sttp.tapir.pickler.PicklerConfiguration +```scala +import sttp.tapir.json.pickler.PicklerConfiguration given customConfiguration: PicklerConfiguration = PicklerConfiguration @@ -94,8 +95,8 @@ given customConfiguration: PicklerConfiguration = Pickler derivation for coproduct types (enums with parameters / sealed hierarchies) works automatically, by adding a `$type` discriminator field with the short class name. -```scala -import sttp.tapir.pickler.PicklerConfiguration +```scala +import sttp.tapir.json.pickler.PicklerConfiguration // encodes a case object as { "$type": "MyType" } given PicklerConfiguration = PicklerConfiguration.default @@ -106,31 +107,31 @@ Selaed hierarchies with all cases being objects are treated differently, conside A discriminator field can be specified for coproducts by providing it in the configuration; this will be only used during automatic and semi-automatic derivation: -```scala -import sttp.tapir.pickler.PicklerConfiguration +```scala +import sttp.tapir.json.pickler.PicklerConfiguration // encodes a case object as { "who_am_i": "full.pkg.path.MyType" } given customConfiguration: PicklerConfiguration = PicklerConfiguration .default .withDiscriminator("who_am_i") - .withFullDiscriminatorValues + .withFullDiscriminatorValues ``` The discriminator will be added as a field to all coproduct child codecs and schemas, if it’s not yet present. The schema of the added field will always be a Schema.string. Finally, the mapping between the discriminator field values and the child schemas will be generated using `Configuration.toDiscriminatorValue(childSchemaName)`. Finally, if the discriminator is a field that’s defined on the base trait (and hence in each implementation), the schemas can be specified as a custom implicit value using the `Pickler.oneOfUsingField` macro, for example (this will also generate the appropriate mappings): -```scala -sealed trait Entity { +```scala +sealed trait Entity: def kind: String -} -case class Person(firstName: String, lastName: String) extends Entity { + +case class Person(firstName: String, lastName: String) extends Entity: def kind: String = "person" -} -case class Organization(name: String) extends Entity { + +case class Organization(name: String) extends Entity: def kind: String = "org" -} + import sttp.tapir.json.pickler.* @@ -159,7 +160,7 @@ Tapir schemas and JSON codecs treats following cases as "enumerations": Such types are handled by `Pickler.derived[T]`: possible values are encoded as simple strings representing the case objects. For example: -```scala +```scala import sttp.tapir.json.pickler.* enum ColorEnum: @@ -205,7 +206,7 @@ pResponse.toCodec.encode( If you need to customize enumeration value encoding, use `Pickler.derivedEnumeration[T]`: -```scala +```scala import sttp.tapir.json.pickler.* enum ColorEnum: diff --git a/generated-doc/out/endpoint/schemas.md b/generated-doc/out/endpoint/schemas.md index 02d606ca51..bd9d635af4 100644 --- a/generated-doc/out/endpoint/schemas.md +++ b/generated-doc/out/endpoint/schemas.md @@ -27,18 +27,18 @@ perform any [validation](validation.md). ## Automatic derivation -Schemas for case classes, sealed traits and their children can be recursively derived. Importing `sttp.tapir.generic.auto._` +Schemas for case classes, sealed traits and their children can be recursively derived. Importing `sttp.tapir.generic.auto.*` (or extending the `SchemaDerivation` trait) enables fully automatic derivation for `Schema`: ```scala import sttp.tapir.Schema -import sttp.tapir.generic.auto._ +import sttp.tapir.generic.auto.* case class Parent(child: Child) case class Child(value: String) // implicit schema used by codecs -implicitly[Schema[Parent]] +summon[Schema[Parent]] ``` If you have a case class which contains some non-standard types (other than strings, number, other case classes, @@ -46,7 +46,7 @@ collections), you only need to provide implicit schemas for them. Using these, t Note that when using [datatypes integrations](integrations.md), respective schemas & codecs must also be imported to enable the derivation, e.g. for [newtype](integrations.html#newtype-integration) you'll have to add -`import sttp.tapir.codec.newtype._` or extend `TapirCodecNewType`. +`import sttp.tapir.codec.newtype.*` or extend `TapirCodecNewType`. ## Semi-automatic derivation @@ -64,8 +64,8 @@ import sttp.tapir.Schema case class Parent(child: Child) case class Child(value: String) -implicit lazy val sChild: Schema[Child] = Schema.derived -implicit lazy val sParent: Schema[Parent] = Schema.derived +given Schema[Child] = Schema.derived +given Schema[Parent] = Schema.derived ``` Note that while schemas for regular types can be safely defined as `val`s, in case of recursive values, the schema @@ -90,9 +90,8 @@ For example: ```scala case class RecursiveTest(data: List[RecursiveTest]) -object RecursiveTest { +object RecursiveTest: implicit def f1Schema: Schema[RecursiveTest] = Schema.derived[RecursiveTest] -} ``` The implicit doesn't have to be defined in the companion object, just anywhere in scope. This applies to cases where @@ -120,7 +119,7 @@ the union type, as it's not possible to generate a runtime check for the generic ### Derivation for string-based constant union types e.g. `type AorB = "a" | "b"` -See [enumerations](enumerations.md#scala-3-string-based-constant-union-types-to-enum) on how to use string-based unions of constant types as enums. +See [enumerations](enumerations.html#scala-3-string-based-constant-union-types-to-enum) on how to use string-based unions of constant types as enums. ## Configuring derivation @@ -131,8 +130,7 @@ representation is described in documentation: ```scala import sttp.tapir.generic.Configuration -implicit val customConfiguration: Configuration = - Configuration.default.withSnakeCaseMemberNames +given Configuration = Configuration.default.withSnakeCaseMemberNames ``` ## Manually providing schemas @@ -141,12 +139,12 @@ Alternatively, `Schema[_]` values can be defined by hand, either for whole case For example, here we state that the schema for `MyCustomType` is a `String`: ```scala -import sttp.tapir._ +import sttp.tapir.* case class MyCustomType() -implicit val schemaForMyCustomType: Schema[MyCustomType] = Schema.string +given Schema[MyCustomType] = Schema.string // or, if the low-level representation is e.g. a number -implicit val anotherSchemaForMyCustomType: Schema[MyCustomType] = Schema(SchemaType.SInteger()) +// given Schema[MyCustomType] = Schema(SchemaType.SInteger()) ``` ## Sealed traits / coproducts @@ -172,7 +170,7 @@ during automatic and semi-automatic derivation: ```scala import sttp.tapir.generic.Configuration -implicit val customConfiguration: Configuration = +given Configuration = Configuration.default.withDiscriminator("who_am_i") ``` @@ -186,17 +184,17 @@ semi-automatic or automatic derivation; in both cases a custom implicit has to b one: ```scala -import sttp.tapir._ +import sttp.tapir.* import sttp.tapir.generic.Derived -import sttp.tapir.generic.auto._ +import sttp.tapir.generic.auto.* sealed trait MyCoproduct case class Child1(s: String) extends MyCoproduct // ... implementations of MyCoproduct ... -implicit val myCoproductSchema: Schema[MyCoproduct] = { +given Schema[MyCoproduct] = val derived = implicitly[Derived[Schema[MyCoproduct]]].value - derived.schemaType match { + derived.schemaType match case s: SchemaType.SCoproduct[_] => derived.copy(schemaType = s.addDiscriminatorField( FieldName("myField"), Schema.string, @@ -206,8 +204,6 @@ implicit val myCoproductSchema: Schema[MyCoproduct] = { ) )) case _ => ??? - } -} ``` Finally, if the discriminator is a field that's defined on the base trait (and hence in each implementation), the @@ -215,23 +211,22 @@ schemas can be specified as a custom implicit value using the `Schema.oneOfUsing for example (this will also generate the appropriate mappings): ```scala -sealed trait Entity { +sealed trait Entity: def kind: String -} -case class Person(firstName: String, lastName: String) extends Entity { + +case class Person(firstName: String, lastName: String) extends Entity: def kind: String = "person" -} -case class Organization(name: String) extends Entity { + +case class Organization(name: String) extends Entity: def kind: String = "org" -} -import sttp.tapir._ +import sttp.tapir.* val sPerson = Schema.derived[Person] val sOrganization = Schema.derived[Organization] -implicit val sEntity: Schema[Entity] = - Schema.oneOfUsingField[Entity, String](_.kind, _.toString)( - "person" -> sPerson, "org" -> sOrganization) +given Schema[Entity] = + Schema.oneOfUsingField[Entity, String](_.kind, _.toString)( + "person" -> sPerson, "org" -> sOrganization) ``` ### Wrapper object discriminators @@ -245,10 +240,10 @@ sealed trait Entity case class Person(firstName: String, lastName: String) extends Entity case class Organization(name: String) extends Entity -import sttp.tapir._ -import sttp.tapir.generic.auto._ // to derive child schemas +import sttp.tapir.* +import sttp.tapir.generic.auto.* // to derive child schemas -implicit val sEntity: Schema[Entity] = Schema.oneOfWrapped[Entity] +given Schema[Entity] = Schema.oneOfWrapped[Entity] ``` The names of the field in the wrapper object will be generated using the implicit `Configuration`. If for some reason @@ -287,13 +282,13 @@ Schemas for products/coproducts (case classes and case class families) can be tr For example: ```scala -import sttp.tapir._ -import sttp.tapir.generic.auto._ +import sttp.tapir.* +import sttp.tapir.generic.auto.* import sttp.tapir.generic.Derived case class Basket(fruits: List[FruitAmount]) case class FruitAmount(fruit: String, amount: Int) -implicit val customBasketSchema: Schema[Basket] = implicitly[Derived[Schema[Basket]]].value +given Schema[Basket] = summon[Derived[Schema[Basket]]].value .modify(_.fruits.each.amount)(_.description("How many fruits?")) ``` @@ -317,21 +312,21 @@ For example, to support an integer wrapped in a value type in a json body, we ne decoders (if that's the json library that we are using), schema information with validator: ```scala -import sttp.tapir._ -import sttp.tapir.generic.auto._ -import sttp.tapir.json.circe._ -import io.circe.{ Encoder, Decoder } -import io.circe.generic.semiauto._ +import sttp.tapir.* +import sttp.tapir.generic.auto.* +import sttp.tapir.json.circe.* +import io.circe.{Encoder, Decoder} +import io.circe.generic.semiauto.* case class Amount(v: Int) extends AnyVal case class FruitAmount(fruit: String, amount: Amount) -implicit val amountSchema: Schema[Amount] = Schema(SchemaType.SInteger()).validate(Validator.min(1).contramap(_.v)) -implicit val amountEncoder: Encoder[Amount] = Encoder.encodeInt.contramap(_.v) -implicit val amountDecoder: Decoder[Amount] = Decoder.decodeInt.map(Amount.apply) +given Schema[Amount] = Schema(SchemaType.SInteger()).validate(Validator.min(1).contramap(_.v)) +given Encoder[Amount] = Encoder.encodeInt.contramap(_.v) +given Decoder[Amount] = Decoder.decodeInt.map(Amount.apply) -implicit val decoder: Decoder[FruitAmount] = deriveDecoder[FruitAmount] -implicit val encoder: Encoder[FruitAmount] = deriveEncoder[FruitAmount] +given Decoder[FruitAmount] = deriveDecoder[FruitAmount] +given Encoder[FruitAmount] = deriveEncoder[FruitAmount] val e: PublicEndpoint[FruitAmount, Unit, Unit, Nothing] = endpoint.in(jsonBody[FruitAmount]) diff --git a/generated-doc/out/endpoint/security.md b/generated-doc/out/endpoint/security.md index 08207affbc..d522c227fa 100644 --- a/generated-doc/out/endpoint/security.md +++ b/generated-doc/out/endpoint/security.md @@ -21,7 +21,7 @@ using one of the methods from `auth`, and arbitrary "regular" inputs, such as pa inputs can contain inputs created through `auth`, though typically this shouldn't be the case. ``` -Currently, the following authentication inputs are available (assuming `import sttp.tapir._`): +Currently, the following authentication inputs are available (assuming `import sttp.tapir.*`): * `auth.apiKey(anotherInput)`: wraps any other input and designates it as an api key. The input is typically a header, cookie or a query parameter @@ -31,7 +31,7 @@ base64-encoded username/password combination, use: `basic[UsernamePassword]`. as a string, use: `bearer[String]`. * `auth.oauth2.authorizationCode(authorizationUrl, scopes, tokenUrl, refreshUrl): EndpointInput[String]`: creates an OAuth2 authorization using authorization code - sign in using an auth service (for documentation, requires defining also -the `oauth2-redirect.html`, see [Generating OpenAPI documentation](..docs/openapi.md)) +the `oauth2-redirect.html`, see [Generating OpenAPI documentation](../docs/openapi.md)) ## Authentication challenges @@ -55,8 +55,8 @@ This feature is available for all server backends *except*: `akka-grpc`, `Armeri Individual endpoints can be annotated with content length limit: ```scala -import sttp.tapir._ -import sttp.tapir.server.model.EndpointExtensions._ +import sttp.tapir.* +import sttp.tapir.server.model.EndpointExtensions.* val limitedEndpoint = endpoint.maxRequestBodyLength(maxBytes = 16384L) ``` diff --git a/generated-doc/out/endpoint/static.md b/generated-doc/out/endpoint/static.md index 878cfc070c..597266f338 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-files" % "1.10.13" ``` ## Files @@ -20,43 +20,33 @@ The easiest way to expose static content from the local filesystem is to use the is parametrised with the path, at which the content should be exposed, as well as the local system path, from which to read the data. -Such an endpoint has to be interpreted using your server interpreter. For example, using the [akka-http](../server/akkahttp.md) interpreter: +Such an endpoint has to be interpreted using your server interpreter. For example, using the [netty-sync](../server/netty.md) interpreter: ```scala -import akka.http.scaladsl.server.Route +import sttp.tapir.* +import sttp.tapir.files.* +import sttp.tapir.server.netty.sync.NettySyncServer -import sttp.tapir._ -import sttp.tapir.files._ -import sttp.tapir.server.akkahttp.AkkaHttpServerInterpreter - -import scala.concurrent.Future -import scala.concurrent.ExecutionContext.Implicits.global - -val filesRoute: Route = AkkaHttpServerInterpreter().toRoute( - staticFilesGetServerEndpoint[Future]("site" / "static")("/home/static/data") -) +NettySyncServer() + .addEndpoint(staticFilesGetServerEndpoint("site" / "static")("/home/static/data")) + .startAndWait() ``` Using the above endpoint, a request to `/site/static/css/styles.css` will try to read the `/home/static/data/css/styles.css` file. -To expose files without a prefix, use `emptyInput`. For example, using the [netty](../server/netty.md) interpreter, the -below exposes the content of `/var/www` at `http://localhost:8080`: +To expose files without a prefix, use `emptyInput`. For example, below exposes the content of `/var/www` at +`http://localhost:8080`: ```scala -import sttp.tapir.server.netty.NettyFutureServer +import sttp.tapir.server.netty.sync.NettySyncServer import sttp.tapir.emptyInput -import sttp.tapir._ -import sttp.tapir.files._ - -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future +import sttp.tapir.* +import sttp.tapir.files.* -NettyFutureServer() - .port(8080) - .addEndpoint(staticFilesGetServerEndpoint[Future](emptyInput)("/var/www")) - .start() - .flatMap(_ => Future.never) +NettySyncServer() + .addEndpoint(staticFilesGetServerEndpoint(emptyInput)("/var/www")) + .startAndWait() ``` A single file can be exposed using `staticFileGetServerEndpoint`. @@ -79,26 +69,25 @@ Endpoint constructor methods for files and resources can receive optional `FileO ```scala import sttp.model.headers.ETag import sttp.tapir.emptyInput -import sttp.tapir._ -import sttp.tapir.files._ - -import scala.concurrent.Future +import sttp.tapir.* +import sttp.tapir.files.* +import sttp.shared.Identity import java.net.URL -val customETag: Option[RangeValue] => URL => Future[Option[ETag]] = ??? +val customETag: Option[RangeValue] => URL => Option[ETag] = ??? val customFileFilter: List[String] => Boolean = ??? -val options: FilesOptions[Future] = +val options: FilesOptions[Identity] = FilesOptions - .default + .default[Identity] // serves file.txt.gz instead of file.txt if available and Accept-Encoding contains "gzip" .withUseGzippedIfAvailable .calculateETag(customETag) .fileFilter(customFileFilter) .defaultFile(List("default.md")) -val endpoint = staticFilesGetServerEndpoint(emptyInput)("/var/www", options) +val endpoint = staticFilesGetServerEndpoint[Identity](emptyInput)("/var/www", options) ``` ## Endpoint description and server logic @@ -119,11 +108,10 @@ The content of [WebJars](https://www.webjars.org) that are available on the clas following routes (here using the `/resources` context path): ```scala -import sttp.tapir._ -import sttp.tapir.files._ - -import scala.concurrent.Future +import sttp.tapir.* +import sttp.tapir.files.* +import sttp.shared.Identity -val webJarRoutes = staticResourcesGetServerEndpoint[Future]("resources")( +val webJarRoutes = staticResourcesGetServerEndpoint[Identity]("resources")( this.getClass.getClassLoader, "META-INF/resources/webjars") ``` diff --git a/generated-doc/out/endpoint/streaming.md b/generated-doc/out/endpoint/streaming.md index 3bc9b7e1cf..703f7339d0 100644 --- a/generated-doc/out/endpoint/streaming.md +++ b/generated-doc/out/endpoint/streaming.md @@ -27,16 +27,14 @@ For example, to specify that the output is an akka-stream, which is a (presumabl mapping to the `Person` class: ```scala -import sttp.tapir._ -import sttp.tapir.generic.auto._ -import sttp.capabilities.akka.AkkaStreams -import akka.stream.scaladsl._ -import akka.util.ByteString +import sttp.tapir.* +import sttp.tapir.generic.auto.* +import sttp.capabilities.pekko.PekkoStreams case class Person(name: String) // copying the derived json schema type -endpoint.out(streamBody(AkkaStreams)(Schema.derived[List[Person]], CodecFormat.Json())) +endpoint.out(streamBody(PekkoStreams)(Schema.derived[List[Person]], CodecFormat.Json())) ``` See also the [runnable streaming example](../examples.md). diff --git a/generated-doc/out/endpoint/validation.md b/generated-doc/out/endpoint/validation.md index 12c62f2648..9f778e670f 100644 --- a/generated-doc/out/endpoint/validation.md +++ b/generated-doc/out/endpoint/validation.md @@ -25,7 +25,7 @@ Validators can also be added to individual inputs/outputs. Behind the scenes, th to add top-level validators this way, rather than modifying the implicit schemas, for example: ```scala -import sttp.tapir._ +import sttp.tapir.* val e = endpoint.in( query[Int]("amount") @@ -36,7 +36,7 @@ val e = endpoint.in( For optional/iterable inputs/outputs, to validate the contained value(s), use: ```scala -import sttp.tapir._ +import sttp.tapir.* query[Option[Int]]("item").validateOption(Validator.min(0)) query[List[Int]]("item").validateIterable(Validator.min(0)) // validates each repeated parameter @@ -47,12 +47,12 @@ query[List[Int]]("item").validateIterable(Validator.min(0)) // validates each re Finally, if you are creating a reusable [codec](codecs.md), a validator can be added to it as well: ```scala -import sttp.tapir._ +import sttp.tapir.* import sttp.tapir.CodecFormat.TextPlain case class MyId(id: String) -implicit val myIdCodec: Codec[String, MyId, TextPlain] = Codec.string +given Codec[String, MyId, TextPlain] = Codec.string .map(MyId(_))(_.id) .validate(Validator.pattern("^[A-Z].*").contramap(_.id)) ``` @@ -95,14 +95,14 @@ converts the enum value to a raw type (typically a string). This can be specifie For example: ```scala -import sttp.tapir._ +import sttp.tapir.* sealed trait Color case object Blue extends Color case object Red extends Color // providing the enum values by hand -implicit def colorSchema: Schema[Color] = Schema.string.validate( +given Schema[Color] = Schema.string.validate( Validator.enumeration(List(Blue, Red), (c: Color) => Some(c.toString.toLowerCase))) ``` diff --git a/generated-doc/out/endpoint/websockets.md b/generated-doc/out/endpoint/websockets.md index 36a85e93ad..a627691ddb 100644 --- a/generated-doc/out/endpoint/websockets.md +++ b/generated-doc/out/endpoint/websockets.md @@ -14,15 +14,15 @@ For example, here's an endpoint where the requests are strings (hence only text are parsed/formatted as json: ```scala -import sttp.tapir._ -import sttp.capabilities.akka.AkkaStreams -import sttp.tapir.json.circe._ -import sttp.tapir.generic.auto._ -import io.circe.generic.auto._ +import sttp.tapir.* +import sttp.capabilities.pekko.PekkoStreams +import sttp.tapir.json.circe.* +import sttp.tapir.generic.auto.* +import io.circe.generic.auto.* case class Response(msg: String, count: Int) endpoint.out( - webSocketBody[String, CodecFormat.TextPlain, Response, CodecFormat.Json](AkkaStreams)) + webSocketBody[String, CodecFormat.TextPlain, Response, CodecFormat.Json](PekkoStreams)) ``` When creating a `webSocketBody`, we need to provide the following parameters: @@ -39,17 +39,17 @@ decoded, but this can be customised through methods on `webSocketBody`. Alternatively, it's possible to obtain a raw pipe transforming `WebSocketFrame`s: ```scala -import akka.stream.scaladsl.Flow -import sttp.tapir._ -import sttp.capabilities.akka.AkkaStreams +import org.apache.pekko.stream.scaladsl.Flow +import sttp.tapir.* +import sttp.capabilities.pekko.PekkoStreams import sttp.capabilities.WebSockets import sttp.ws.WebSocketFrame -endpoint.out(webSocketBodyRaw(AkkaStreams)): PublicEndpoint[ +endpoint.out(webSocketBodyRaw(PekkoStreams)): PublicEndpoint[ Unit, Unit, Flow[WebSocketFrame, WebSocketFrame, Any], - AkkaStreams with WebSockets] + PekkoStreams with WebSockets] ``` Such a pipe by default doesn't handle ping-pong frames automatically, doesn't concatenate fragmented flames, and @@ -60,7 +60,7 @@ Request/response schemas can be customised through `.requestsSchema` and `.respo ## Interpreting as a sever When interpreting a web socket endpoint as a server, the [server logic](../server/logic.md) needs to provide a -streaming-specific pipe from requests to responses. E.g. in akka's case, this will be `Flow[REQ, RESP, Any]`. +streaming-specific pipe from requests to responses. E.g. in Pekko's case, this will be `Flow[REQ, RESP, Any]`. Refer to the documentation of interpreters for more details, as not all interpreters support all settings. @@ -83,4 +83,4 @@ interpreters. ## Next -Read on about [datatypes integrations](integrations.md). \ No newline at end of file +Read on about [datatypes integrations](integrations.md). diff --git a/generated-doc/out/endpoint/xml.md b/generated-doc/out/endpoint/xml.md index 4ad792cda0..3720817958 100644 --- a/generated-doc/out/endpoint/xml.md +++ b/generated-doc/out/endpoint/xml.md @@ -31,24 +31,19 @@ import sttp.tapir.{Codec, EndpointIO, Schema, stringBodyUtf8AnyFormat} import scala.xml.{NodeSeq, XML} -trait TapirXmlScalaxb { +trait TapirXmlScalaxb: case class XmlElementLabel(label: String) def xmlBody[T: XMLFormat: Schema](implicit l: XmlElementLabel): EndpointIO.Body[String, T] = stringBodyUtf8AnyFormat(scalaxbCodec[T]) - implicit def scalaxbCodec[T: XMLFormat: Schema](implicit label: XmlElementLabel): XmlCodec[T] = { + given (using XmlFormat[T], Schema[T], XmlElementLabel): XmlCodec[T] = Codec.xml((s: String) => - try { - Value(fromXML[T](XML.loadString(s))) - } catch { - case e: Exception => Error(s, e) - } + try Value(fromXML[T](XML.loadString(s))) + catch case e: Exception => Error(s, e) )((t: T) => { - val nodeSeq: NodeSeq = toXML[T](obj = t, elementLabel = label.label, scope = defaultScope) + val nodeSeq: NodeSeq = toXML[T](obj = t, elementLabel = summon[XmlElementLabel].label, scope = defaultScope) nodeSeq.toString() }) - } -} ``` This creates `XmlCodec[T]` that would encode / decode the types with `XMLFormat`, `Schema` and with `XmlElementLabel` provided in scope. It also introduces `xmlBody` helper method, which allows you to easily express, that the declared endpoint consumes or returns XML. @@ -70,19 +65,18 @@ Usage example: import sttp.tapir.{PublicEndpoint, endpoint} import cats.effect.IO import generated.Outer // import may differ depending on location of generated code -import sttp.tapir.generic.auto._ // needed for Schema derivation +import sttp.tapir.generic.auto.* // needed for Schema derivation import sttp.tapir.server.ServerEndpoint -object Endpoints { - import xml._ // imports tapir related serialization / deserialization logic +object Endpoints: + import xml.* // imports tapir related serialization / deserialization logic - implicit val label: XmlElementLabel = XmlElementLabel("outer") // `label` is needed by scalaxb code to properly encode the top node of the xml + given XmlElementLabel = XmlElementLabel("outer") // `label` is needed by scalaxb code to properly encode the top node of the xml val xmlEndpoint: PublicEndpoint[Outer, Unit, Outer, Any] = endpoint.post // `Outer` is a class generated by scalaxb based on .xsd file. .in("xml") .in(xmlBody[Outer]) .out(xmlBody[Outer]) -} ``` If the generation of OpenAPI documentation is required, consider adding OpenAPI doc extension on schema providing XML @@ -93,7 +87,7 @@ For more information on adding OpenAPI doc extension in tapir refer to [document Adding xml namespace doc extension to `Outer`'s `Schema` example: ```scala case class XmlNamespace(namespace: String) -implicit val outerSchemaWithXmlNamespace: Schema[Outer] = implicitly[Derived[Schema[Outer]]].value +given Schema[Outer] = summon[Derived[Schema[Outer]]].value .docsExtension("xml", XmlNamespace("http://www.example.com/innerouter")) ``` diff --git a/generated-doc/out/examples.md b/generated-doc/out/examples.md index 04090a8a26..be8151896a 100644 --- a/generated-doc/out/examples.md +++ b/generated-doc/out/examples.md @@ -1,35 +1,18 @@ -# Examples +# Examples by category -The [examples](https://github.com/softwaremill/tapir/tree/master/examples/src/main/scala/sttp/tapir/examples) and [examples2](https://github.com/softwaremill/tapir/tree/master/examples2/src/main/scala/sttp/tapir/examples2) sub-projects (the latter containing Scala 2-only code) contains a number of runnable tapir usage examples, using various interpreters and showcasing different features. +The Tapir repository contains a number of how-to guides. If you're missing an example for your use-case, please let us +know by [reporting an issue](https://github.com/softwaremill/tapir)! -## Generate a tapir project +Each example is fully self-contained and can be run using [scala-cli](https://scala-cli.virtuslab.org) (you just need +to copy the content of the file, apart from scala-cli, no additional setup is required!). Hopefully this will make +experimenting with Tapir as frictionless as possible! -You can generate a simple tapir-based project using chosen features, build tool and effect system using [adopt-tapir](https://adopt-tapir.softwaremill.com). +Examples are tagged with the stack being used (Direct-style, cats-effect, ZIO, Future), server implementation, generated +documentation format etc. -Alternatively, you can generate a stub of a tapir-based application directly from the command line with `sbt new softwaremill/tapir.g8`. +```{eval-rst} +.. include:: includes/examples_list.md + :parser: markdown +``` -## Third-party examples -* http4s interpreter: [todo-backend](https://github.com/lolgab/snunit-tapir-example) -* quickstart using http4s: [a gitter8 template](https://codeberg.org/wegtam/http4s-tapir.g8). A new project can be created using: `sbt new https://codeberg.org/wegtam/http4s-tapir.g8.git` -* Scala Native application, [using Nginx Unit](https://github.com/lolgab/snunit-tapir-example). - -## Blogs, articles - -* [Migrating from Akka HTTP to tapir](https://softwaremill.com/migrating-from-akka-http-to-tapir/) -* [Benchmarking Tapir: Part 1](https://softwaremill.com/benchmarking-tapir-part-1/) -* [Benchmarking Tapir: Part 2](https://softwaremill.com/benchmarking-tapir-part-2/) -* [Tapir 1.0 released](https://softwaremill.com/tapir-1-0-released/) -* [Security improvements in tapir 0.19](https://softwaremill.com/security-improvements-in-tapir-0-19/) -* [Tapir serverless: a proof of concept](https://blog.softwaremill.com/tapir-serverless-a-proof-of-concept-6b8c9de4d396) -* [Designing tapir's WebSockets support](https://blog.softwaremill.com/designing-tapirs-websockets-support-ff1573166368) -* [Three easy endpoints](https://blog.softwaremill.com/three-easy-endpoints-a6cbd52b0a6e) -* [tAPIr's Endpoint meets ZIO's IO](https://blog.softwaremill.com/tapirs-endpoint-meets-zio-s-io-3278099c5e10) -* [Describe, then interpret: HTTP endpoints using tapir](https://blog.softwaremill.com/describe-then-interpret-http-endpoints-using-tapir-ac139ba565b0) -* [Functional pancakes](https://blog.softwaremill.com/functional-pancakes-cf70023f0dcb) - -## Videos - -* [ScalaLove 2020: Your HTTP endpoints are data, as well!](https://www.youtube.com/watch?v=yuQNgZgSFIc&t=944s) -* [Scalar 2020: A Functional Scala Stack For 2020](https://www.youtube.com/watch?v=DGlkap5kzGU) -* [ScalaWorld 2019: Designing Programmer-Friendly APIs](https://www.youtube.com/watch?v=I3loMuHnYqw) diff --git a/generated-doc/out/external.md b/generated-doc/out/external.md new file mode 100644 index 0000000000..5a9a07188f --- /dev/null +++ b/generated-doc/out/external.md @@ -0,0 +1,33 @@ +# Articles, videos, other examples + +## Generate a tapir project + +You can generate a simple tapir-based project using chosen features, build tool and effect system using [adopt-tapir](https://adopt-tapir.softwaremill.com). + +Alternatively, you can generate a stub of a tapir-based application directly from the command line with `sbt new softwaremill/tapir.g8`. + +## Third-party examples + +* http4s interpreter: [todo-backend](https://github.com/lolgab/snunit-tapir-example) +* quickstart using http4s: [a gitter8 template](https://codeberg.org/wegtam/http4s-tapir.g8). A new project can be created using: `sbt new https://codeberg.org/wegtam/http4s-tapir.g8.git` +* Scala Native application, [using Nginx Unit](https://github.com/lolgab/snunit-tapir-example). + +## Blogs, articles + +* [Migrating from Akka HTTP to tapir](https://softwaremill.com/migrating-from-akka-http-to-tapir/) +* [Benchmarking Tapir: Part 1](https://softwaremill.com/benchmarking-tapir-part-1/) +* [Benchmarking Tapir: Part 2](https://softwaremill.com/benchmarking-tapir-part-2/) +* [Tapir 1.0 released](https://softwaremill.com/tapir-1-0-released/) +* [Security improvements in tapir 0.19](https://softwaremill.com/security-improvements-in-tapir-0-19/) +* [Tapir serverless: a proof of concept](https://blog.softwaremill.com/tapir-serverless-a-proof-of-concept-6b8c9de4d396) +* [Designing tapir's WebSockets support](https://blog.softwaremill.com/designing-tapirs-websockets-support-ff1573166368) +* [Three easy endpoints](https://blog.softwaremill.com/three-easy-endpoints-a6cbd52b0a6e) +* [tAPIr's Endpoint meets ZIO's IO](https://blog.softwaremill.com/tapirs-endpoint-meets-zio-s-io-3278099c5e10) +* [Describe, then interpret: HTTP endpoints using tapir](https://blog.softwaremill.com/describe-then-interpret-http-endpoints-using-tapir-ac139ba565b0) +* [Functional pancakes](https://blog.softwaremill.com/functional-pancakes-cf70023f0dcb) + +## Videos + +* [ScalaLove 2020: Your HTTP endpoints are data, as well!](https://www.youtube.com/watch?v=yuQNgZgSFIc&t=944s) +* [Scalar 2020: A Functional Scala Stack For 2020](https://www.youtube.com/watch?v=DGlkap5kzGU) +* [ScalaWorld 2019: Designing Programmer-Friendly APIs](https://www.youtube.com/watch?v=I3loMuHnYqw) diff --git a/generated-doc/out/generate.md b/generated-doc/out/generate.md new file mode 100644 index 0000000000..29302de31a --- /dev/null +++ b/generated-doc/out/generate.md @@ -0,0 +1,21 @@ +# Generate a Tapir project + +Not sure how to start? + +We recommend the defaults below (Direct-style stack & Netty server); this requires Java 21+. Otherwise, +you might also try the Future stack + Netty, which works on Java 11+. + +If you'd like to include a JSON endpoint, using the [jsoniter](https://github.com/plokhotnyuk/jsoniter-scala) library +might a good choice! + +```{eval-rst} +.. raw:: html + + +``` diff --git a/generated-doc/out/generator/sbt-openapi-codegen.md b/generated-doc/out/generator/sbt-openapi-codegen.md index b67442d70f..ff4bbba9ed 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.10.12") +addSbtPlugin("com.softwaremill.sttp.tapir" % "sbt-openapi-codegen" % "1.10.13") ``` Enable the plugin for your project in the `build.sbt`: @@ -50,9 +50,9 @@ openapiAdditionalPackages Nil Addit The general usage is; ```scala -import sttp.apispec.openapi.circe.yaml._ -import sttp.tapir.generated._ -import sttp.tapir.docs.openapi._ +import sttp.apispec.openapi.circe.yaml.* +import sttp.tapir.generated.* +import sttp.tapir.docs.openapi.* val docs = TapirGeneratedEndpoints.generatedEndpoints.toOpenAPI("My Bookshop", "1.0") ``` @@ -89,8 +89,9 @@ If `openapiUseHeadTagForObjectName = true`, then the `GET /foo` and `GET /bar` `Baz.scala` file, containing a single `object Baz` with those endpoint definitions; the `PUT /foo` endpoint, by dint of having no tags, would be output to the `TapirGeneratedEndpoints` file, along with any schema and parameter definitions. -Files can be generated from multiple openapi schemas if `openapiAdditionalPackages` is configured; for example -```sbt +Files can be generated from multiple openapi schemas if `openapiAdditionalPackages` is configured; for example: + +```scala openapiAdditionalPackages := List( "sttp.tapir.generated.v1" -> baseDirectory.value / "src" / "main" / "resources" / "openapi_v1.yml") ``` diff --git a/generated-doc/out/includes/examples_list.md b/generated-doc/out/includes/examples_list.md new file mode 100644 index 0000000000..6e62dcbe7d --- /dev/null +++ b/generated-doc/out/includes/examples_list.md @@ -0,0 +1,103 @@ +## Hello, World! + +* [A demo of Tapir's capabilities](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/booksExample.scala) circe sttp3 Swagger UI Future Pekko HTTP +* [A demo of Tapir's capabilities](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/booksPicklerExample.scala) Pickler sttp3 Swagger UI Future Netty +* [Exposing an endpoint using the Armeria server](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/helloWorldArmeriaServer.scala) Future Armeria +* [Exposing an endpoint using the Netty server (Direct-style variant)](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/helloWorldNettySyncServer.scala) Direct Netty +* [Exposing an endpoint using the Netty server (Future variant)](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/helloWorldNettyFutureServer.scala) Future Netty +* [Exposing an endpoint using the Netty server (cats-effect variant)](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/HelloWorldNettyCatsServer.scala) cats-effect Netty +* [Exposing an endpoint using the Pekko HTTP server](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/helloWorldPekkoServer.scala) Future Pekko HTTP +* [Exposing an endpoint using the ZIO HTTP server](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/HelloWorldZioHttpServer.scala) ZIO ZIO JSON ZIO HTTP +* [Exposing an endpoint using the ZIO HTTP server](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/ZioExampleZioHttpServer.scala) Swagger UI ZIO circe zio-http +* [Exposing an endpoint using the built-in JDK HTTP server](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/helloWorldJdkHttpServer.scala) Direct JDK Http +* [Exposing an endpoint using the http4s server](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala) Future http4s +* [Exposing an endpoint using the http4s server](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala) Swagger UI ZIO circe http4s +* [Exposing an endpoint, defined with ZIO and depending on services in the environment, using the http4s server](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala) Swagger UI ZIO circe http4s +* [Extending a base endpoint (which has the security logic provided), with server logic](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala) ZIO http4s + +## Client interpreter + +* [Interpreting an endpoint as an http4s client](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/client/Http4sClientExample.scala) circe cats-effect + +## 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 +* [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 + +## Error handling + +* [Customising errors that are reported on decode failures (e.g. invalid or missing query parameter)](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/errors/customErrorsOnDecodeFailurePekkoServer.scala) Future Pekko HTTP +* [Error and successful outputs](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/errors/errorOutputsPekkoServer.scala) Future circe Pekko HTTP +* [Error reporting provided by Iron type refinements](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/errors/IronRefinementErrorsNettyServer.scala) circe cats-effect Netty +* [Extending a base secured endpoint with error variants, using union types](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/errors/ErrorUnionTypesHttp4sServer.scala) circe cats-effect http4s + +## Logging + +* [Logging using a correlation id](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/logging/ZioLoggingWithCorrelationIdNettyServer.scala) ZIO Netty + +## Multipart + +* [Uploading a multipart form, with text and file parts](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/multipart/multipartFormUploadPekkoServer.scala) Future Pekko HTTP + +## Observability + +* [Reporting DataDog metrics](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/observability/datadogMetricsExample.scala) Future circe Netty +* [Reporting OpenTelemetry metrics](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/observability/openTelemetryMetricsExample.scala) Future circe Netty +* [Reporting Prometheus metrics](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/observability/ZioMetricsExample.scala) ZIO ZIO HTTP +* [Reporting Prometheus metrics](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/observability/prometheusMetricsExample.scala) Future circe Netty + +## OpenAPI documentation + +* [Adding OpenAPI documentation extensions](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/openapi/openapiExtensions.scala) circe +* [Documenting multiple endpoints](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/openapi/MultipleEndpointsDocumentationHttp4sServer.scala) Swagger UI cats-effect circe http4s +* [Documenting multiple endpoints](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/openapi/multipleEndpointsDocumentationPekkoServer.scala) Swagger UI Future circe Pekko HTTP +* [Exposing documentation using ReDoc](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/openapi/RedocZioHttpServer.scala) ReDoc ZIO circe ZIO HTTP +* [Exposing documentation using ReDoc](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/openapi/RedocContextPathHttp4sServer.scala) ReDoc cats-effect http4s +* [Securing Swagger UI using OAuth 2](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/openapi/swaggerUIOAuth2PekkoServer.scala) Swagger UI Future Pekko HTTP + +## Schemas + +* [Customising a derived schema, using annotations, and using implicits](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/schema/customisingSchemas.scala) Swagger UI Future circe Netty + +## Security + +* [CORS interceptor](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/security/corsInterceptorPekkoServer.scala) Future Pekko HTTP +* [HTTP basic authentication](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/security/basicAuthenticationPekkoServer.scala) Future Pekko HTTP +* [Interceptor verifying externally added security credentials](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/security/externalSecurityInterceptor.scala) Future Netty +* [Login using OAuth2, authorization code flow](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/security/OAuth2GithubHttp4sServer.scala) cats-effect circe http4s +* [Separating security and server logic, with a reusable base endpoint](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/security/serverSecurityLogicPekko.scala) Future Pekko HTTP +* [Separating security and server logic, with a reusable base endpoint, accepting & refreshing credentials via cookies](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/security/serverSecurityLogicRefreshCookiesPekko.scala) Future Pekko HTTP +* [Separating security and server logic, with a reusable base endpoint, accepting & refreshing credentials via cookies](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/security/ServerSecurityLogicZio.scala) ZIO ZIO HTTP + +## Static content + +* [Serving static files from a directory](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/static_content/staticContentFromFilesNettyServer.scala) Direct Netty +* [Serving static files from a directory, with range requests](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/static_content/staticContentFromFilesPekkoServer.scala) Future Pekko HTTP +* [Serving static files from resources](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/static_content/staticContentFromResourcesPekkoServer.scala) Future Pekko HTTP +* [Serving static files secured with a bearer token](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/static_content/staticContentSecurePekkoServer.scala) Future Pekko HTTP + +## Status code + +* [Serving static files from a directory](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/status_code/statusCodeNettyServer.scala) Direct Netty + +## Streaming + +* [Proxy requests, handling bodies as fs2 streams](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/streaming/ProxyHttp4sFs2Server.scala) cats-effect http4s +* [Stream response as a Pekko stream](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/streaming/streamingPekkoServer.scala) Future Pekko HTTP +* [Stream response as a ZIO stream](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/streaming/StreamingNettyZioServer.scala) ZIO Netty +* [Stream response as a ZIO stream](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/streaming/StreamingZioHttpServer.scala) ZIO ZIO HTTP +* [Stream response as an fs2 stream](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/streaming/StreamingHttp4sFs2Server.scala) cats-effect http4s +* [Stream response as an fs2 stream](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/streaming/StreamingNettyFs2Server.scala) cats-effect Netty + +## Testing + +* [Test endpoints using the MockServer client](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/testing/SttpMockServerClientExample.scala) circe +* [Test endpoints using the TapirStubInterpreter](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/testing/PekkoServerStubInterpreterExample.scala) Future Pekko HTTP + +## WebSocket + +* [Describe and implement a WebSocket endpoint](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketNettySyncServer.scala) Direct Netty +* [Describe and implement a WebSocket endpoint](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/websocket/webSocketPekkoServer.scala) Future Pekko HTTP +* [Describe and implement a WebSocket endpoint](https://github.com/softwaremill/tapir/tree/master//examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketHttp4sServer.scala) AsyncAPI cats-effect circe http4s \ No newline at end of file diff --git a/generated-doc/out/index.md b/generated-doc/out/index.md index cba78b3e12..5c520c636c 100644 --- a/generated-doc/out/index.md +++ b/generated-doc/out/index.md @@ -1,128 +1,65 @@ # tapir
-

Declarative, type-safe web endpoints library.

+

Rapid development of self-documenting APIs

tapir
## Intro -With tapir, you can describe HTTP API endpoints as immutable Scala values. Each endpoint can contain a number of -input and output parameters. An endpoint specification can be interpreted as: - -* a server, given the "business logic": a function, which computes output parameters based on input parameters. - Currently supported: - * [Akka HTTP](server/akkahttp.md) `Route`s/`Directive`s - * [Http4s](server/http4s.md) `HttpRoutes[F]` (using cats-effect or [ZIO](server/zio-http4s.md)) - * [Netty](server/netty.md) (using `Future`s, cats-effect or ZIO) - * [Helidon Níma](server/nima.md) (using JVM 21 Virtual Threads and direct style) - * [Finatra](server/finatra.md) `http.Controller` - * [Pekko HTTP](server/pekkohttp.md) `Route`s/`Directive`s - * [Play](server/play.md) `Route` - * [Vert.X](server/vertx.md) `Router => Route` (using `Future`s, cats-effect or ZIO) - * [ZIO Http](server/ziohttp.md) `Http` - * [Armeria](server/armeria.md) `HttpServiceWithRoutes` (using `Future`s, cats-effect or ZIO) - * [JDK HTTP](server/jdkhttp.md) `HttpHandler` (simple, synchronous API only) - * [aws](server/aws.md) through Lambda/SAM/Terraform - * [gRPC](grpc.md) -* a client, which is a function from input parameters to output parameters. - Currently supported: - * [sttp](client/sttp.md) - * [Play](client/play.md) - * [Http4s](client/http4s.md) -* documentation. Currently supported: - * [OpenAPI](docs/openapi.md) - * [AsyncAPI](docs/asyncapi.md) - * [Json Schema](docs/json-schema.md) - -Depending on how you prefer to explore the library, take a look at one of the [examples](examples.md) or read on -for a more detailed description of how tapir works! ScalaDocs are available at [javadoc.io](https://www.javadoc.io/doc/com.softwaremill.sttp.tapir). +Tapir is a library to describe HTTP APIs, expose them as a server, consume as a client, and automatically document +using open standards. -## Why tapir? - -* **type-safety**: compile-time guarantees, develop-time completions, read-time information -* **declarative**: separate the shape of the endpoint (the "what"), from the server logic (the "how") -* **OpenAPI / Swagger integration**: generate documentation from endpoint descriptions -* **observability**: leverage the metadata to report rich metrics and tracing information -* **abstraction**: re-use common endpoint definitions, as well as individual inputs/outputs -* **library, not a framework**: integrates with your stack +Tapir is fast and developer-friendly. The endpoint definition APIs are crafted with readability and discoverability in +mind. Our Netty-based server is one of the best-performing Scala HTTP servers available. -## Adopt a tapir - -```{eval-rst} -.. raw:: html - - +```scala +endpoint + .get.in("hello").in(query[String]("name")) + .out(stringBody) + .handleSuccess(name => s"Hello, $name!") ``` -## Availability +Tapir integrates with all major Scala stacks, so you can use your favorite approach to Functional Programming, while +leveraging all the benefits that Tapir brings! -Tapir is available: +Seamless integration with the Scala and HTTP ecosystems is one of Tapir's major strengths: -* all modules - Scala 2.12 and 2.13 on the JVM (Java 11+) -* selected modules - Scala 3 on the JVM (Java 11+) -* selected modules - Scala 2.12, 2.13 and 3 using Scala.JS -* selected modules - Scala 2.12, 2.13 and 3 using Scala Native +* all popular Scala HTTP server implementations are supported. You can define your entire API using Tapir, or expose +Tapir-managed routes alongside "native" ones. This is especially useful when gradually adopting Tapir, or using it for +selected use-cases. +* the Scala ecosystem is rich with libraries leveraging its type-safety and enhancing the developer's toolbox, +that's why Tapir provides integrations with many of such custom type, JSON and observability libraries +* documentation can be generated in the [OpenAPI](docs/openapi.md), [AsyncAPI](docs/asyncapi.md) and [JSON Schema](docs/json-schema.md) formats -Tapir is licensed under Apache2, the source code is [available on GitHub](https://github.com/softwaremill/tapir). +Depending on how you'd prefer to explore Tapir, this documentation has three main sections: -## Adopters +1. There's a number of [tutorials](tutorials/01_hello_world.md), which provide a gentle introduction to the library +2. Nothing compares to tinkering with working code, that's why we've prepared [runnable examples](examples.md), + covering solutions to many "everyday" problems +3. Finally, the reference documentation describes all of Tapir's aspects in depth - take a look at the menu on + the left, starting with the "Endpoints" section -Is your company already using tapir? We're continually expanding the "adopters" section in the documentation; the more the merrier! It would be great to feature your company's logo, but in order to do that, we'll need written permission to avoid any legal misunderstandings. +ScalaDocs are available at [javadoc.io](https://www.javadoc.io/doc/com.softwaremill.sttp.tapir). -Please email us at [tapir@softwaremill.com](mailto:tapir@softwaremill.com) from your company's email with a link to your logo (if we can use it, of course!) or with details who to kindly ask for permission to feature the logo in tapir's documentation. We'll handle the rest. +Tapir is licensed under Apache2, the source code is [available on GitHub](https://github.com/softwaremill/tapir). -Thank you! +## Why tapir? -
-Adobe -Swisscom -Swissborg -
-
-Kaizo -Process Street -Tranzzo -
-
-Kelkoo group -SoftwareMill -Carvana -
-
-Moneyfarm -Ocado -Wegtam -
-
-Broad -Kensu -Colisweb -
-
-iceo -dpg -hunters -
-
-moia -pits -hootsuite -
+* **type-safety**: compile-time guarantees, develop-time completions, read-time information +* **declarative**: separate the shape of the endpoint (the "what"), from the server logic (the "how") +* **OpenAPI / Swagger integration**: generate documentation from endpoint descriptions +* **observability**: leverage the metadata to report rich metrics and tracing information +* **abstraction**: re-use common endpoint definitions, as well as individual inputs/outputs +* **library, not a framework**: integrates with your stack ## Code teaser ```scala -import sttp.tapir._ -import sttp.tapir.generic.auto._ -import sttp.tapir.json.circe._ -import io.circe.generic.auto._ +import sttp.tapir.* +import sttp.tapir.generic.auto.* +import sttp.tapir.json.circe.* +import io.circe.generic.auto.* type Limit = Int type AuthToken = String @@ -144,17 +81,17 @@ val booksListing: PublicEndpoint[(BooksQuery, Limit, AuthToken), String, List[Bo // Generate OpenAPI documentation -import sttp.apispec.openapi.circe.yaml._ +import sttp.apispec.openapi.circe.yaml.* import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter val docs = OpenAPIDocsInterpreter().toOpenAPI(booksListing, "My Bookshop", "1.0") println(docs.toYaml) -// Convert to akka-http Route +// Convert to pekko-http Route -import sttp.tapir.server.akkahttp.AkkaHttpServerInterpreter -import akka.http.scaladsl.server.Route +import sttp.tapir.server.pekkohttp.PekkoHttpServerInterpreter +import org.apache.pekko.http.scaladsl.server.Route import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits.global @@ -163,14 +100,14 @@ def bookListingLogic(bfy: BooksQuery, at: AuthToken): Future[Either[String, List[Book]]] = Future.successful(Right(List(Book("The Sorrows of Young Werther")))) -val booksListingRoute: Route = AkkaHttpServerInterpreter() +val booksListingRoute: Route = PekkoHttpServerInterpreter() .toRoute(booksListing.serverLogic((bookListingLogic _).tupled)) // Convert to sttp Request import sttp.tapir.client.sttp.SttpClientInterpreter -import sttp.client3._ +import sttp.client3.* val booksListingRequest: Request[DecodeResult[Either[String, List[Book]]], Any] = SttpClientInterpreter() @@ -188,16 +125,6 @@ sttp is a family of Scala HTTP-related projects, and currently includes: * [sttp shared](https://github.com/softwaremill/sttp-shared): shared web socket, FP abstractions, capabilities and streaming code. * [sttp apispec](https://github.com/softwaremill/sttp-apispec): OpenAPI, AsyncAPI and JSON Schema models. -## Sponsors - -Development and maintenance of sttp tapir is sponsored by [SoftwareMill](https://softwaremill.com), a software development and consulting company. We help clients scale their business through software. Our areas of expertise include backends, distributed systems, blockchain, machine learning and data analytics. - -[![](https://files.softwaremill.com/logo/logo.png "SoftwareMill")](https://softwaremill.com) - -## Commercial Support - -We offer commercial support for sttp and related technologies, as well as development services. [Contact us](https://softwaremill.com/contact/) to learn more about our offer! - ## Table of contents ```{eval-rst} @@ -206,8 +133,10 @@ We offer commercial support for sttp and related technologies, as well as develo :caption: Getting started quickstart - examples - stability + generate + adopters + support + scala_2_3_platforms .. toctree:: :maxdepth: 2 @@ -219,6 +148,14 @@ We offer commercial support for sttp and related technologies, as well as develo tutorials/04_errors tutorials/05_multiple_inputs_outputs tutorials/06_error_variants + tutorials/07_cats_effect + +.. toctree:: + :maxdepth: 2 + :caption: How-to's + + examples + external .. toctree:: :maxdepth: 2 @@ -247,6 +184,7 @@ We offer commercial support for sttp and related technologies, as well as develo :maxdepth: 2 :caption: Server interpreters + server/overview server/akkahttp server/http4s server/zio-http4s @@ -300,11 +238,12 @@ We offer commercial support for sttp and related technologies, as well as develo :maxdepth: 2 :caption: Other subjects - other_interpreters - mytapir - grpc - troubleshooting - migrating - goals - contributing + other/stability + other/other_interpreters + other/mytapir + other/grpc + other/troubleshooting + other/migrating + other/goals + other/contributing diff --git a/generated-doc/out/contributing.md b/generated-doc/out/other/contributing.md similarity index 100% rename from generated-doc/out/contributing.md rename to generated-doc/out/other/contributing.md diff --git a/generated-doc/out/goals.md b/generated-doc/out/other/goals.md similarity index 100% rename from generated-doc/out/goals.md rename to generated-doc/out/other/goals.md diff --git a/generated-doc/out/grpc.md b/generated-doc/out/other/grpc.md similarity index 100% rename from generated-doc/out/grpc.md rename to generated-doc/out/other/grpc.md diff --git a/generated-doc/out/migrating.md b/generated-doc/out/other/migrating.md similarity index 98% rename from generated-doc/out/migrating.md rename to generated-doc/out/other/migrating.md index bbd915f12a..b0904b538f 100644 --- a/generated-doc/out/migrating.md +++ b/generated-doc/out/other/migrating.md @@ -14,11 +14,11 @@ - If your code sets `badRequestOnPathErrorIfPathShapeMatches = true` to override the default `false`, you can just remove this in tapir 1.5, it is the new default. - Similarly, if your code sets `.badRequestOnDecodeFailure` on endpoint path input, just remove this attribute. - If your code doesn't change this parameter and you update tapir, you should expect shape-matched path decoding failures to always become 400s, without attempting the next endpoint unless explicitly specified. -- If you want to override this behavior and force trying the next endpoint, add `.onDecodeFailureNextEndpoint` to the input where you expect such handling. See [error handling page](server/errors.md) for details. +- If you want to override this behavior and force trying the next endpoint, add `.onDecodeFailureNextEndpoint` to the input where you expect such handling. See [error handling page](../server/errors.md) for details. ## From 1.2 to 1.3 -- Static content endpoints from `sttp.tapir.static._` are deprecated in favor of the new `tapir-files` module. New methods are in `sttp.tapir.files._`: `staticFilesGetServerEndpoint`, `staticFilesHeadServerEndpoint`, `staticFilesServerEndpoints`, `staticResourcesGetServerEndpoint`, `staticResourcesHeadServerEndpoint`, `staticResourcesServerEndpoints`, etc. See the [updated documentation](endpoint/static.md). +- Static content endpoints from `sttp.tapir.static._` are deprecated in favor of the new `tapir-files` module. New methods are in `sttp.tapir.files._`: `staticFilesGetServerEndpoint`, `staticFilesHeadServerEndpoint`, `staticFilesServerEndpoints`, `staticResourcesGetServerEndpoint`, `staticResourcesHeadServerEndpoint`, `staticResourcesServerEndpoints`, etc. See the [updated documentation](../endpoint/static.md). - Respectively, use `sttp.tapir.files.FilesOptions` instead of `sttp.tapir.static.FilesOptions` - the `cats` integration module has been split into `cats` and `cats-effect`, with the latter containing the `CatsMonadError` class, providing a bridge between the sttp-internal `MonadError` and the cats-effect `Sync` typeclass. If you've been using this directly, you might need to update your dependencies. diff --git a/generated-doc/out/mytapir.md b/generated-doc/out/other/mytapir.md similarity index 85% rename from generated-doc/out/mytapir.md rename to generated-doc/out/other/mytapir.md index c7fdfed9f8..b8b31baca3 100644 --- a/generated-doc/out/mytapir.md +++ b/generated-doc/out/other/mytapir.md @@ -9,7 +9,7 @@ a single-import whenever you want to use tapir. For example: ```scala object MyTapir extends Tapir - with AkkaHttpServerInterpreter + with PekkoHttpServerInterpreter with SttpClientInterpreter with OpenAPIDocsInterpreter with SchemaDerivation @@ -23,9 +23,9 @@ Then, a single `import MyTapir._` and all Tapir data types and interpreter metho You might also define an alias for `Endpoint`, with the capabilities that your endpoints use, e.g.: ```scala -import sttp.capabilities.akka.AkkaStreams +import sttp.capabilities.pekko.PekkoStreams import sttp.capabilities.WebSockets import sttp.tapir.Endpoint -type MyEndpoint[A, I, E, O] = Endpoint[A, I, E, O, AkkaStreams with WebSockets] -``` \ No newline at end of file +type MyEndpoint[A, I, E, O] = Endpoint[A, I, E, O, PekkoStreams with WebSockets] +``` diff --git a/generated-doc/out/other_interpreters.md b/generated-doc/out/other/other_interpreters.md similarity index 100% rename from generated-doc/out/other_interpreters.md rename to generated-doc/out/other/other_interpreters.md diff --git a/generated-doc/out/stability.md b/generated-doc/out/other/stability.md similarity index 100% rename from generated-doc/out/stability.md rename to generated-doc/out/other/stability.md diff --git a/generated-doc/out/troubleshooting.md b/generated-doc/out/other/troubleshooting.md similarity index 100% rename from generated-doc/out/troubleshooting.md rename to generated-doc/out/other/troubleshooting.md diff --git a/generated-doc/out/quickstart.md b/generated-doc/out/quickstart.md index 01925674d4..fc2b5c3dcf 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-core" % "1.10.13" ``` This will import only the core classes needed to create endpoint descriptions. To generate a server or a client, you @@ -13,7 +13,7 @@ Many of tapir functionalities come as builder methods in the main package, hence you import the main package entirely, i.e.: ```scala -import sttp.tapir._ +import sttp.tapir.* ``` Finally, type: @@ -24,12 +24,3 @@ endpoint. and see where auto-complete gets you! -## Scala 2.12 - -Partial unification is now enabled by default from Scala 2.13. However, if you're using Scala 2.12 or older, and don't -have it already, you'll want to to enable partial unification in the compiler (alternatively, you'll need to manually -provide type arguments in some cases). In sbt, this is: - -```scala -scalacOptions += "-Ypartial-unification" -``` \ No newline at end of file diff --git a/generated-doc/out/scala_2_3_platforms.md b/generated-doc/out/scala_2_3_platforms.md new file mode 100644 index 0000000000..db64bdab16 --- /dev/null +++ b/generated-doc/out/scala_2_3_platforms.md @@ -0,0 +1,63 @@ +# Scala 2, Scala 3; JVM, JS & Native + +Tapir is available for Scala 3.3+, Scala 2.13 and Scala 2.12, on the JVM, JS and Native platforms. + +Note that not all modules are available for all combinations of the above. This specifically applies to Scala.JS and +Scala Native, where support is limited. The JVM modules require Java 11+, with a couple of exceptions, which require +Java 21+ - this is marked in the documentation. + +## In the documentation & examples + +The documentation & examples are written & compiled using Scala 3. To compile example code with Scala 2, some +adjustments might be necessary: + +* For wildcard imports, use `_` instead of `*`, e.g. instead of `import sttp.tapir.*`, use `import sttp.tapir._` +* For the main method, instead of `@main`, use an `object MyApp extends App`, e.g.: + +```scala +// in Scala 3: +@main def myExample(): Unit = /* body */ + +// in Scala 2: +object MyExample extends App { + /* body */ +} +``` + +* Instead of `given` definitions, use `implicit val` or `implicit def` (for codecs, schemas etc.). E.g.: + +```scala +// in Scala 3: +given Schema[MyType] = Schema.derived + +// in Scala 2: +implicit val myTypeSchema: Schema[MyType] = Schema.derived +``` + +* Use curly braces around class & method definitions. E.g.: + +```scala +// in Scala 3: +class MyClass: + def myMethod(): Unit = + val z = 2 + z + 2 + +// in Scala 2: +class MyClass { + def myMethod(): Unit = { + val z = 2 + z + 2 + } +} +``` + +## Scala 2.12 + +Partial unification is now enabled by default from Scala 2.13. However, if you're using Scala 2.12 or older, and don't +have it already, you'll want to to enable partial unification in the compiler (alternatively, you'll need to manually +provide type arguments in some cases). In sbt, this is: + +```scala +scalacOptions += "-Ypartial-unification" +``` diff --git a/generated-doc/out/server/akkahttp.md b/generated-doc/out/server/akkahttp.md index c2f3a21e3f..55c2b61ca8 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-akka-http-server" % "1.10.13" ``` 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.10.12" exclude("com.typesafe.akka", "akka-stream_2.12") +"com.softwaremill.sttp.tapir" %% "tapir-akka-http-server" % "1.10.13" exclude("com.typesafe.akka", "akka-stream_2.12") ``` Now import the object: @@ -55,7 +55,7 @@ tapir-generated directive. Edge-case endpoints, which require special logic not expressible using tapir, can be implemented directly using akka-http. For example: -```scala +```scala import sttp.tapir._ import sttp.tapir.server.akkahttp.AkkaHttpServerInterpreter import akka.http.scaladsl.server._ diff --git a/generated-doc/out/server/armeria.md b/generated-doc/out/server/armeria.md index d1a7b7c35b..496197e955 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-armeria-server" % "1.10.13" ``` and import the object: @@ -22,31 +22,28 @@ The `toService` method require a single, or a list of `ServerEndpoint`s, which c [server logic](logic.md) to an endpoint. ```scala -import sttp.tapir._ +import sttp.tapir.* import sttp.tapir.server.armeria.ArmeriaFutureServerInterpreter import scala.concurrent.Future import com.linecorp.armeria.server.Server -object Main { - // JVM entry point that starts the HTTP server - def main(args: Array[String]): Unit = { - val tapirEndpoint: PublicEndpoint[(String, Int), Unit, String, Any] = ??? // your definition here - def logic(s: String, i: Int): Future[Either[Unit, String]] = ??? // your logic here - val tapirService = ArmeriaFutureServerInterpreter().toService(tapirEndpoint.serverLogic((logic _).tupled)) - val server = Server - .builder() - .service(tapirService) // your endpoint is bound to the server - .build() - server.start().join() - } -} +// JVM entry point that starts the HTTP server - uncommment @main to run +/* @main */ def armeriaSerer(): Unit = + val tapirEndpoint: PublicEndpoint[(String, Int), Unit, String, Any] = ??? // your definition here + def logic(s: String, i: Int): Future[Either[Unit, String]] = ??? // your logic here + val tapirService = ArmeriaFutureServerInterpreter().toService(tapirEndpoint.serverLogic((logic _).tupled)) + val server = Server + .builder() + .service(tapirService) // your endpoint is bound to the server + .build() + server.start().join() ``` This interpreter also supports streaming using Armeria Streams which is fully compatible with Reactive Streams: ```scala import sttp.capabilities.armeria.ArmeriaStreams -import sttp.tapir._ +import sttp.tapir.* import sttp.tapir.server.armeria.ArmeriaFutureServerInterpreter import scala.concurrent.Future import com.linecorp.armeria.common.HttpData @@ -59,9 +56,8 @@ val streamingResponse: PublicEndpoint[Int, Unit, Publisher[HttpData], ArmeriaStr .in(query[Int]("key")) .out(streamTextBody(ArmeriaStreams)(CodecFormat.TextPlain())) -def streamLogic(foo: Int): Future[Publisher[HttpData]] = { +def streamLogic(foo: Int): Future[Publisher[HttpData]] = Future.successful(StreamMessage.of(HttpData.ofUtf8("hello"), HttpData.ofUtf8("world"))) -} val tapirService = ArmeriaFutureServerInterpreter().toService(streamingResponse.serverLogicSuccess(streamLogic)) ``` @@ -75,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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-armeria-server-cats" % "1.10.13" ``` to use this interpreter with Cats Effect typeclasses. @@ -88,14 +84,15 @@ This object contains the `toService(e: ServerEndpoint[Fs2Streams[F], F])` method An HTTP server can then be started as in the following example: ```scala -import sttp.tapir._ +import sttp.tapir.* import sttp.tapir.server.armeria.cats.ArmeriaCatsServerInterpreter -import cats.effect._ +import cats.effect.* import cats.effect.std.Dispatcher import com.linecorp.armeria.server.Server +import java.util.concurrent.CompletableFuture -object Main extends IOApp { - override def run(args: List[String]): IO[ExitCode] = { +object Main extends IOApp: + override def run(args: List[String]): IO[ExitCode] = val tapirEndpoint: PublicEndpoint[String, Unit, String, Any] = ??? def logic(req: String): IO[Either[Unit, String]] = ??? @@ -116,23 +113,21 @@ object Main extends IOApp { } } )({ server => - IO.fromCompletableFuture(IO(server.closeAsync())).void + IO.fromCompletableFuture(IO(server.closeAsync().asInstanceOf[CompletableFuture[Unit]])) }) } .use(_ => IO.never) - } -} ``` This interpreter also supports streaming using FS2 streams: ```scala import sttp.capabilities.fs2.Fs2Streams -import sttp.tapir._ +import sttp.tapir.* import sttp.tapir.server.armeria.cats.ArmeriaCatsServerInterpreter -import cats.effect._ +import cats.effect.* import cats.effect.std.Dispatcher -import fs2._ +import fs2.* val streamingResponse: Endpoint[Unit, Int, Unit, Stream[IO, Byte], Fs2Streams[IO]] = endpoint @@ -140,9 +135,8 @@ val streamingResponse: Endpoint[Unit, Int, Unit, Stream[IO, Byte], Fs2Streams[IO .in(query[Int]("times")) .out(streamTextBody(Fs2Streams[IO])(CodecFormat.TextPlain())) -def streamLogic(times: Int): IO[Stream[IO, Byte]] = { +def streamLogic(times: Int): IO[Stream[IO, Byte]] = IO.pure(Stream.chunk(Chunk.array("Hello world!".getBytes)).repeatN(times)) -} def dispatcher: Dispatcher[IO] = ??? @@ -154,7 +148,7 @@ val tapirService = ArmeriaCatsServerInterpreter(dispatcher).toService(streamingR Add the following dependency ```scala -"com.softwaremill.sttp.tapir" %% "tapir-armeria-server-zio" % "1.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-armeria-server-zio" % "1.10.13" ``` to use this interpreter with ZIO. @@ -169,14 +163,15 @@ An HTTP server can then be started as in the following example: ```scala import com.linecorp.armeria.server.Server -import sttp.tapir._ +import sttp.tapir.* import sttp.tapir.server.armeria.zio.ArmeriaZioServerInterpreter -import sttp.tapir.ztapir._ +import sttp.tapir.ztapir.* import zio.{ExitCode, Runtime, UIO, URIO, ZIO, ZIOAppDefault} +import java.util.concurrent.CompletableFuture -object Main extends ZIOAppDefault { - override def run: URIO[Any, ExitCode] = { - implicit val runtime = Runtime.default +object Main extends ZIOAppDefault: + override def run: URIO[Any, ExitCode] = + given Runtime[Any] = Runtime.default val tapirEndpoint: PublicEndpoint[String, Unit, String, Any] = ??? def logic(key: String): UIO[String] = ??? @@ -190,9 +185,8 @@ object Main extends ZIOAppDefault { server.start().thenApply[Server](_ => server) } - ZIO.scoped(ZIO.acquireRelease(s)(server => ZIO.fromCompletableFuture(server.closeAsync()).orDie) *> ZIO.never).exitCode - } -} + ZIO.scoped(ZIO.acquireRelease(s)(server => + ZIO.fromCompletableFuture(server.closeAsync().asInstanceOf[CompletableFuture[Unit]]).orDie) *> ZIO.never).exitCode ``` This interpreter supports streaming using ZStreams. diff --git a/generated-doc/out/server/aws.md b/generated-doc/out/server/aws.md index 0c4ba33f64..0686a799c7 100644 --- a/generated-doc/out/server/aws.md +++ b/generated-doc/out/server/aws.md @@ -5,12 +5,9 @@ an [AWS Lambda](https://docs.aws.amazon.com/apigateway/latest/developerguide/htt This approach, known as the Fat Lambda function, utilizes a single lambda function for deploying multiple endpoints. To invoke the function, HTTP requests can be proxied through [AWS API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/welcome.html). +To configure API Gateway routes, and the Lambda function, tools like [AWS SAM](https://aws.amazon.com/serverless/sam/), [AWS CDK](https://aws.amazon.com/cdk/) or [Terraform](https://www.terraform.io/) can be used, to automate cloud deployments. -To configure API Gateway routes, and the Lambda function, tools like [AWS SAM](https://aws.amazon.com/serverless/sam/) -, [AWS CDK](https://aws.amazon.com/cdk/) or [Terraform](https://www.terraform.io/) can be used, to automate cloud deployments. - -For an overview of how this works in more detail, see [this blog post](https://blog.softwaremill.com/tapir-serverless-a-proof-of-concept-6b8c9de4d396) -. +For an overview of how this works in more detail, see [this blog post](https://blog.softwaremill.com/tapir-serverless-a-proof-of-concept-6b8c9de4d396). ## Runtime & Server interpreters @@ -32,8 +29,8 @@ These are corresponding classes for each of the supported runtime: To start using any of the above add the following dependency: -```sbt -"com.softwaremill.sttp.tapir" %% "tapir-aws-lambda" % "1.10.12" +```scala +"com.softwaremill.sttp.tapir" %% "tapir-aws-lambda" % "1.10.13" ``` ## Deployment @@ -43,10 +40,10 @@ 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: -```sbt -"com.softwaremill.sttp.tapir" %% "tapir-aws-sam" % "1.10.12" -"com.softwaremill.sttp.tapir" %% "tapir-aws-terraform" % "1.10.12" -"com.softwaremill.sttp.tapir" %% "tapir-aws-cdk" % "1.10.12" +```scala +"com.softwaremill.sttp.tapir" %% "tapir-aws-sam" % "1.10.13" +"com.softwaremill.sttp.tapir" %% "tapir-aws-terraform" % "1.10.13" +"com.softwaremill.sttp.tapir" %% "tapir-aws-cdk" % "1.10.13" ``` ### Examples diff --git a/generated-doc/out/server/errors.md b/generated-doc/out/server/errors.md index a83cba3783..45f6927942 100644 --- a/generated-doc/out/server/errors.md +++ b/generated-doc/out/server/errors.md @@ -21,12 +21,12 @@ If the business logic signals errors as exceptions, some or all can be recovered For example: ```scala -import sttp.tapir._ +import sttp.tapir.* import sttp.tapir.server.netty.NettyFutureServerInterpreter -import scala.concurrent.Future -import scala.util._ +import scala.concurrent.{ExecutionContext, Future} +import scala.util.* -implicit val ec = scala.concurrent.ExecutionContext.global +given ExecutionContext = ExecutionContext.global type ErrorInfo = String def logic(s: String): Future[Int] = ??? @@ -118,13 +118,13 @@ Moreover, when using the `DefaultDecodeFailureHandler`, decode failure handling basis, by setting an attribute. For example: ```scala -import sttp.tapir._ +import sttp.tapir.* // bringing into scope the onDecodeFailureNextEndpoint extension method -import sttp.tapir.server.interceptor.decodefailure.DefaultDecodeFailureHandler.OnDecodeFailure._ +import sttp.tapir.server.interceptor.decodefailure.DefaultDecodeFailureHandler.OnDecodeFailure.* case class UserId(value: String) -object UserId { - implicit val codec: Codec[String, UserId, CodecFormat.TextPlain] = Codec.string.mapDecode(raw => +object UserId: + given Codec[String, UserId, CodecFormat.TextPlain] = Codec.string.mapDecode(raw => UserId.make(raw) match { case Left(error) => DecodeResult.Error(raw, new IllegalArgumentException(s"Invalid User value ($raw), failed with $error")) @@ -134,7 +134,6 @@ object UserId { def make(in: String): Either[String, UserId] = if (in.length > 5) Right(new UserId(in)) else Left("Too short") -} // If your codec for UserId fails, allow checking other endpoints for possible matches, like /customer/some_special_case endpoint.in("customer" / path[UserId]("user_id").onDecodeFailureNextEndpoint) @@ -151,12 +150,12 @@ default ones for you. We'll need to provide both the endpoint output which should be used for error messages, along with the output's value: ```scala -import sttp.tapir._ +import sttp.tapir.* import sttp.tapir.server.model.ValuedEndpointOutput import sttp.tapir.server.netty.NettyFutureServerOptions -import sttp.tapir.generic.auto._ -import sttp.tapir.json.circe._ -import io.circe.generic.auto._ +import sttp.tapir.generic.auto.* +import sttp.tapir.json.circe.* +import io.circe.generic.auto.* case class MyFailure(msg: String) def myFailureResponse(m: String): ValuedEndpointOutput[_] = diff --git a/generated-doc/out/server/finatra.md b/generated-doc/out/server/finatra.md index c5cb18197b..83e002b8b4 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-finatra-server" % "1.10.13" ``` 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-finatra-server-cats" % "1.10.13" ``` and import the object: @@ -80,4 +80,4 @@ val aRoute: FinatraRoute = ??? class MyController extends Controller with TapirController { addTapirRoute(aRoute) } -``` \ No newline at end of file +``` diff --git a/generated-doc/out/server/http4s.md b/generated-doc/out/server/http4s.md index 8158ea3aad..fa16832829 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-http4s-server" % "1.10.13" ``` and import the object: @@ -19,7 +19,7 @@ The `toRoutes` and `toHttp` methods require a single, or a list of `ServerEndpoi The server logic should use a cats-effect-support `F[_]` effect type. For example: ```scala -import sttp.tapir._ +import sttp.tapir.* import sttp.tapir.server.http4s.Http4sServerInterpreter import cats.effect.IO import org.http4s.HttpRoutes @@ -71,17 +71,17 @@ using `withHttpWebSocketApp`, for example: ```scala import sttp.capabilities.WebSockets import sttp.capabilities.fs2.Fs2Streams -import sttp.tapir._ +import sttp.tapir.* import sttp.tapir.server.http4s.Http4sServerInterpreter import cats.effect.IO import org.http4s.HttpRoutes import org.http4s.blaze.server.BlazeServerBuilder import org.http4s.server.Router import org.http4s.server.websocket.WebSocketBuilder2 -import fs2._ +import fs2.* import scala.concurrent.ExecutionContext -implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global +given ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global val wsEndpoint: PublicEndpoint[Unit, Unit, Pipe[IO, String, String], Fs2Streams[IO] with WebSockets] = endpoint.get.in("count").out(webSocketBody[String, CodecFormat.TextPlain, String, CodecFormat.TextPlain](Fs2Streams[IO])) @@ -90,7 +90,7 @@ val wsRoutes: WebSocketBuilder2[IO] => HttpRoutes[IO] = Http4sServerInterpreter[IO]().toWebSocketRoutes(wsEndpoint.serverLogicSuccess[IO](_ => ???)) BlazeServerBuilder[IO] - .withExecutionContext(ec) + .withExecutionContext(summon[ExecutionContext]) .bindHttp(8080, "localhost") .withHttpWebSocketApp(wsb => Router("/" -> wsRoutes(wsb)).orNotFound) ``` @@ -104,7 +104,7 @@ For example, to define an endpoint that returns event stream: ```scala import cats.effect.IO import sttp.model.sse.ServerSentEvent -import sttp.tapir._ +import sttp.tapir.* import sttp.tapir.server.http4s.{Http4sServerInterpreter, serverSentEventsBody} val sseEndpoint = endpoint.get.out(serverSentEventsBody[IO]) @@ -123,8 +123,8 @@ with a dedicated context-extracting input, `.contextIn`. Endpoints using such in For example: ```scala -import sttp.tapir._ -import sttp.tapir.server.http4s._ +import sttp.tapir.* +import sttp.tapir.server.http4s.* import cats.effect.IO import org.http4s.ContextRoutes diff --git a/generated-doc/out/server/jdkhttp.md b/generated-doc/out/server/jdkhttp.md index 290ea9da9a..8b37b61dcc 100644 --- a/generated-doc/out/server/jdkhttp.md +++ b/generated-doc/out/server/jdkhttp.md @@ -5,13 +5,13 @@ To expose endpoints using the (`com.sun.net.httpserver`), first add the following dependency: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-jdkhttp-server" % "1.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-jdkhttp-server" % "1.10.13" ``` Then, import the package: ```scala -import sttp.tapir.server.jdkhttp._ +import sttp.tapir.server.jdkhttp.* ``` and use `JdkHttpServer().addEndpoints` to expose server endpoints. @@ -25,8 +25,8 @@ original `serverLogic` methods and also because names are shorter. For example: ```scala -import sttp.tapir._ -import sttp.tapir.server.jdkhttp._ +import sttp.tapir.* +import sttp.tapir.server.jdkhttp.* val helloWorld = endpoint .get diff --git a/generated-doc/out/server/logic.md b/generated-doc/out/server/logic.md index 441345f590..1f7d7eaf6a 100644 --- a/generated-doc/out/server/logic.md +++ b/generated-doc/out/server/logic.md @@ -38,7 +38,7 @@ converted to a function using a single argument using `.tupled`, or that you'll to extract the parameters: ```scala -import sttp.tapir._ +import sttp.tapir.* import sttp.tapir.server.ServerEndpoint import scala.concurrent.Future @@ -63,7 +63,7 @@ Both a single server endpoint, and multiple endpoints can be interpreted as a se endpoints can be converted to a Netty route: ```scala -import sttp.tapir._ +import sttp.tapir.* import sttp.tapir.server.netty.{NettyFutureServerInterpreter, FutureRoute} import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits.global @@ -90,13 +90,13 @@ errors which are subtypes of `E`. Any others will be propagated without changes. For example: ```scala -import sttp.tapir._ +import sttp.tapir.* import scala.concurrent.Future case class MyError(msg: String) extends Exception val testEndpoint = endpoint .in(query[Boolean]("fail")) - .errorOut(stringBody.map(MyError)(_.msg)) + .errorOut(stringBody.map(MyError(_))(_.msg)) .out(stringBody) .serverLogicRecoverErrors { fail => if (fail) { @@ -141,11 +141,11 @@ provided. For example, we can create a partial server endpoint given the security logic, and an endpoint with security inputs: ```scala -import sttp.tapir._ -import sttp.tapir.server._ -import scala.concurrent.Future +import sttp.tapir.* +import sttp.tapir.server.* +import scala.concurrent.{ExecutionContext, Future} -implicit val ec = scala.concurrent.ExecutionContext.global +implicit val ec: ExecutionContext = ExecutionContext.global case class User(name: String) def authLogic(token: String): Future[Either[Int, User]] = Future { @@ -207,8 +207,8 @@ with an error output (for security errors) and the security logic to add. This a before the security logic defined in the endpoint so far (if any). For example: ```scala -import sttp.tapir._ -import sttp.tapir.files._ +import sttp.tapir.* +import sttp.tapir.files.* import scala.concurrent.Future import sttp.model.StatusCode diff --git a/generated-doc/out/server/netty.md b/generated-doc/out/server/netty.md index 5555c4d324..e03a3d6826 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-netty-server" % "1.10.13" // if you want to use Java 21 Loom virtual threads in direct style: -"com.softwaremill.sttp.tapir" %% "tapir-netty-server-sync" % "1.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-netty-server-sync" % "1.10.13" // if you are using cats-effect: -"com.softwaremill.sttp.tapir" %% "tapir-netty-server-cats" % "1.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-netty-server-cats" % "1.10.13" // if you are using zio: -"com.softwaremill.sttp.tapir" %% "tapir-netty-server-zio" % "1.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-netty-server-zio" % "1.10.13" ``` Then, use: @@ -29,7 +29,7 @@ to an endpoint. For example: ```scala -import sttp.tapir._ +import sttp.tapir.* import sttp.tapir.server.netty.{NettyFutureServer, NettyFutureServerBinding} import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future @@ -92,14 +92,14 @@ To create a web socket endpoint, use Tapir's `out(webSocketBody)` output type: ```scala import cats.effect.kernel.Resource import cats.effect.{IO, ResourceApp} -import cats.syntax.all._ +import cats.syntax.all.* import fs2.Pipe import sttp.capabilities.fs2.Fs2Streams -import sttp.tapir._ +import sttp.tapir.* import sttp.tapir.server.netty.cats.NettyCatsServer import sttp.ws.WebSocketFrame -import scala.concurrent.duration._ +import scala.concurrent.duration.* object WebSocketsNettyCatsServer extends ResourceApp.Forever { @@ -159,7 +159,7 @@ You can customize this behavior in `NettyConfig`: ```scala import sttp.tapir.server.netty.NettyConfig -import scala.concurrent.duration._ +import scala.concurrent.duration.* // adjust the waiting time to your needs val config = NettyConfig.default.withGracefulShutdownTimeout(5.seconds) @@ -172,8 +172,8 @@ val config2 = NettyConfig.default.noGracefulShutdown There is possibility to use Domain socket instead of TCP for handling traffic. ```scala +import sttp.tapir.* import sttp.tapir.server.netty.{NettyFutureServer, NettyFutureDomainSocketBinding} -import sttp.tapir.{endpoint, query, stringBody} import java.nio.file.Paths import scala.concurrent.ExecutionContext.Implicits.global diff --git a/generated-doc/out/server/nima.md b/generated-doc/out/server/nima.md index e2688c2f2a..9edf43b86a 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-nima-server" % "1.10.13" ``` Loom-managed concurrency uses direct style instead of effect wrappers like `Future[T]` or `IO[T]`. Because of this, @@ -19,7 +19,7 @@ Such endpoints are then processed through `NimaServerInterpreter` in order to ob ```scala import io.helidon.webserver.WebServer -import sttp.tapir._ +import sttp.tapir.* import sttp.shared.Identity import sttp.tapir.server.nima.NimaServerInterpreter diff --git a/generated-doc/out/server/observability.md b/generated-doc/out/server/observability.md index e16a79ef8c..2f5b7644ca 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-prometheus-metrics" % "1.10.13" ``` `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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-opentelemetry-metrics" % "1.10.13" ``` 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-datadog-metrics" % "1.10.13" ``` 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-zio-metrics" % "1.10.13" ``` Metrics have been integrated into ZIO core in ZIO2. @@ -251,14 +251,12 @@ libraryDependencies += "dev.zio" %% "zio-metrics-connectors" % "2.0.0-RC6" Example zio metrics prometheus publisher style tapir metrics endpoint. ```scala import sttp.tapir.{endpoint, stringBody} -import zio._ +import zio.* import zio.metrics.connectors.MetricsConfig import zio.metrics.connectors.prometheus.{PrometheusPublisher, prometheusLayer, publisherLayer} import zio.metrics.jvm.DefaultJvmMetrics - -object ZioEndpoint { - +object ZioEndpoint: /** DefaultJvmMetrics.live.orDie >+> is optional if you want JVM metrics */ private val layer = DefaultJvmMetrics.live.orDie >+> ZLayer.make[PrometheusPublisher]( ZLayer.succeed(MetricsConfig(1.seconds)), @@ -279,5 +277,4 @@ object ZioEndpoint { val metricsEndpoint = endpoint.get.in("metrics").out(stringBody).serverLogicSuccess(_ => getMetricsEffect) -} ``` diff --git a/generated-doc/out/server/options.md b/generated-doc/out/server/options.md index 20d7a3d97b..681652ecd1 100644 --- a/generated-doc/out/server/options.md +++ b/generated-doc/out/server/options.md @@ -10,23 +10,23 @@ Each interpreter can be configured using an options object, which includes: * additional user-provided [interceptors](interceptors.md) To use custom server options pass them as an argument to the interpreter's `apply` method. -For example, for `AkkaHttpServerOptions` and `AkkaHttpServerInterpreter`: +For example, for `PekkoHttpServerOptions` and `PekkoHttpServerInterpreter`: ```scala import sttp.tapir.server.interceptor.decodefailure.DecodeFailureHandler -import sttp.tapir.server.akkahttp.AkkaHttpServerOptions -import sttp.tapir.server.akkahttp.AkkaHttpServerInterpreter +import sttp.tapir.server.pekkohttp.PekkoHttpServerOptions +import sttp.tapir.server.pekkohttp.PekkoHttpServerInterpreter import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future val customDecodeFailureHandler: DecodeFailureHandler[Future] = ??? -val customServerOptions: AkkaHttpServerOptions = AkkaHttpServerOptions +val customServerOptions: PekkoHttpServerOptions = PekkoHttpServerOptions .customiseInterceptors .decodeFailureHandler(customDecodeFailureHandler) .options - -AkkaHttpServerInterpreter(customServerOptions) + +PekkoHttpServerInterpreter(customServerOptions) ``` ## Hiding authenticated endpoints @@ -39,17 +39,17 @@ returned instead by using a different decode failure handler. For example, using ```scala import sttp.tapir.server.interceptor.decodefailure.DefaultDecodeFailureHandler -import sttp.tapir.server.akkahttp.AkkaHttpServerOptions -import sttp.tapir.server.akkahttp.AkkaHttpServerInterpreter +import sttp.tapir.server.pekkohttp.PekkoHttpServerOptions +import sttp.tapir.server.pekkohttp.PekkoHttpServerInterpreter import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future -val customServerOptions: AkkaHttpServerOptions = AkkaHttpServerOptions +val customServerOptions: PekkoHttpServerOptions = PekkoHttpServerOptions .customiseInterceptors .decodeFailureHandler(DefaultDecodeFailureHandler.hideEndpointsWithAuth[Future]) .options - -AkkaHttpServerInterpreter(customServerOptions) + +PekkoHttpServerInterpreter(customServerOptions) ``` Note however, that it can still be possible to discover the existence of certain endpoints using timing attacks. diff --git a/generated-doc/out/server/overview.md b/generated-doc/out/server/overview.md new file mode 100644 index 0000000000..50af6aac36 --- /dev/null +++ b/generated-doc/out/server/overview.md @@ -0,0 +1,23 @@ +# Overview of server integrations + +Server interpreters require the endpoint descriptions to be combined with "business logic": functions, which compute +an endpoint's output parameters based on input parameters. + +Tapir integrates with a number of HTTP server implementations, through **server interpreters**. We recommend starting +with the Netty-based server. However, if you already have experience with another server, or are using one in your +project already, just continue doing so, and enjoy seamless Tapir integration! + +Currently supported: +* [Netty](netty.md) (using direct-style, `Future`s, cats-effect or ZIO) +* [Http4s](http4s.md) `HttpRoutes[F]` (using cats-effect or [ZIO](zio-http4s.md)) +* [Pekko HTTP](pekkohttp.md) `Route`s/`Directive`s +* [ZIO Http](ziohttp.md) `Http` +* [Play](play.md) `Route` +* [Akka HTTP](akkahttp.md) `Route`s/`Directive`s +* [Helidon Níma](nima.md) (using JVM 21 Virtual Threads and direct style) +* [Finatra](finatra.md) `http.Controller` +* [Vert.X](vertx.md) `Router => Route` (using `Future`s, cats-effect or ZIO) +* [Armeria](armeria.md) `HttpServiceWithRoutes` (using `Future`s, cats-effect or ZIO) +* [JDK HTTP](jdkhttp.md) `HttpHandler` (simple, synchronous API only) +* [aws](aws.md) through Lambda/SAM/Terraform +* [gRPC](../other/grpc.md) diff --git a/generated-doc/out/server/pekkohttp.md b/generated-doc/out/server/pekkohttp.md index be9fdcdf0a..1f7837a20a 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-pekko-http-server" % "1.10.13" ``` 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.10.12" exclude("org.apache.pekko", "pekko-stream_2.12") +"com.softwaremill.sttp.tapir" %% "tapir-pekko-http-server" % "1.10.13" exclude("org.apache.pekko", "pekko-stream_2.12") ``` Now import the object: @@ -28,7 +28,7 @@ The `toRoute` method requires a single, or a list of `ServerEndpoint`s, which ca For example: ```scala -import sttp.tapir._ +import sttp.tapir.* import sttp.tapir.server.pekkohttp.PekkoHttpServerInterpreter import scala.concurrent.Future import org.apache.pekko.http.scaladsl.server.Route @@ -56,9 +56,9 @@ Edge-case endpoints, which require special logic not expressible using tapir, ca using pekko-http. For example: ```scala -import sttp.tapir._ +import sttp.tapir.* import sttp.tapir.server.pekkohttp.PekkoHttpServerInterpreter -import org.apache.pekko.http.scaladsl.server._ +import org.apache.pekko.http.scaladsl.server.* import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits.global @@ -103,7 +103,7 @@ For example, to define an endpoint that returns event stream: ```scala import org.apache.pekko.stream.scaladsl.Source import sttp.model.sse.ServerSentEvent -import sttp.tapir._ +import sttp.tapir.* import sttp.tapir.server.pekkohttp.{PekkoHttpServerInterpreter, serverSentEventsBody} import scala.concurrent.Future diff --git a/generated-doc/out/server/play.md b/generated-doc/out/server/play.md index a6e84a5856..625fddc74c 100644 --- a/generated-doc/out/server/play.md +++ b/generated-doc/out/server/play.md @@ -6,7 +6,7 @@ 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-play29-server" % "1.10.13" ``` and (if you don't already depend on Play) @@ -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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-play-server" % "1.10.13" ``` and (if you don't already depend on Play) @@ -58,13 +58,13 @@ The `toRoutes` method requires a single, or a list of `ServerEndpoint`s, which c ```scala import org.apache.pekko.stream.Materializer import play.api.routing.Router.Routes -import sttp.tapir._ +import sttp.tapir.* import sttp.tapir.server.play.PlayServerInterpreter import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future -implicit val materializer: Materializer = ??? +given Materializer = ??? def countCharacters(s: String): Future[Either[Unit, Int]] = Future(Right[Unit, Int](s.length)) @@ -90,22 +90,19 @@ diverges a bit comparing to other interpreters. An HTTP server can then be started as in the following example: ```scala -import play.core.server._ +import play.core.server.* import play.api.routing.Router.Routes val aRoute: Routes = ??? -object Main { - // JVM entry point that starts the HTTP server - def main(args: Array[String]): Unit = { - val playConfig = ServerConfig(port = - sys.props.get("http.port").map(_.toInt).orElse(Some(9000)) - ) - NettyServer.fromRouterWithComponents(playConfig) { components => - aRoute - } +// JVM entry point that starts the HTTP server - uncomment @main to run +/* @main */ def playServer(): Unit = + val playConfig = ServerConfig(port = + sys.props.get("http.port").map(_.toInt).orElse(Some(9000)) + ) + NettyServer.fromRouterWithComponents(playConfig) { components => + aRoute } -} ``` ### As part of an existing Play application @@ -118,11 +115,9 @@ First, add a line like following in the `routes` files: ``` Then create a class like this: ```scala -class ApiRouter @Inject() () extends SimpleRouter { - override def routes: Routes = { +class ApiRouter @Inject() () extends SimpleRouter: + override def routes: Routes = anotherRoutes.orElse(tapirGeneratedRoutes) - } -} ``` Find more details about how to bind a `Router` to your application in the [Play framework documentation](https://www.playframework.com/documentation/2.8.x/ScalaSirdRouter#Binding-sird-Router). diff --git a/generated-doc/out/server/vertx.md b/generated-doc/out/server/vertx.md index 1d3e54db42..2f3c3a07ef 100644 --- a/generated-doc/out/server/vertx.md +++ b/generated-doc/out/server/vertx.md @@ -8,13 +8,13 @@ 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-vertx-server" % "1.10.13" ``` to use this interpreter with `Future`. Then import the object: ```scala -import sttp.tapir.server.vertx.VertxFutureServerInterpreter._ +import sttp.tapir.server.vertx.VertxFutureServerInterpreter.* ``` This object contains the following methods: @@ -26,27 +26,24 @@ In practice, routes will be mounted on a router, this router can then be used as An HTTP server can then be started as in the following example: ```scala -import sttp.tapir._ +import sttp.tapir.* import sttp.tapir.server.vertx.VertxFutureServerInterpreter -import sttp.tapir.server.vertx.VertxFutureServerInterpreter._ +import sttp.tapir.server.vertx.VertxFutureServerInterpreter.* import io.vertx.core.Vertx -import io.vertx.ext.web._ +import io.vertx.ext.web.* import scala.concurrent.{Await, Future} -import scala.concurrent.duration._ - -object Main { - // JVM entry point that starts the HTTP server - def main(args: Array[String]): Unit = { - val vertx = Vertx.vertx() - val server = vertx.createHttpServer() - val router = Router.router(vertx) - val anEndpoint: PublicEndpoint[(String, Int), Unit, String, Any] = ??? // your definition here - def logic(s: String, i: Int): Future[Either[Unit, String]] = ??? // your logic here - val attach = VertxFutureServerInterpreter().route(anEndpoint.serverLogic((logic _).tupled)) - attach(router) // your endpoint is now attached to the router, and the route has been created - Await.result(server.requestHandler(router).listen(9000).asScala, Duration.Inf) - } -} +import scala.concurrent.duration.* + +// JVM entry point that starts the HTTP server - uncomment @main to run +/* @main */ def vertxServer(): Unit = + val vertx = Vertx.vertx() + val server = vertx.createHttpServer() + val router = Router.router(vertx) + val anEndpoint: PublicEndpoint[(String, Int), Unit, String, Any] = ??? // your definition here + def logic(s: String, i: Int): Future[Either[Unit, String]] = ??? // your logic here + val attach = VertxFutureServerInterpreter().route(anEndpoint.serverLogic((logic _).tupled)) + attach(router) // your endpoint is now attached to the router, and the route has been created + Await.result(server.requestHandler(router).listen(9000).asScala, Duration.Inf) ``` ## Configuration @@ -63,13 +60,13 @@ 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-vertx-server-cats" % "1.10.13" ``` to use this interpreter with Cats Effect typeclasses. Then import the object: ```scala -import sttp.tapir.server.vertx.cats.VertxCatsServerInterpreter._ +import sttp.tapir.server.vertx.cats.VertxCatsServerInterpreter.* ``` This object contains the `route[F[_]](e: ServerEndpoint[Fs2Streams[F], F])` method, which returns a function `Router => Route` that will create a route, with a handler attached, matching the endpoint definition. Errors will be recovered automatically. @@ -77,15 +74,15 @@ This object contains the `route[F[_]](e: ServerEndpoint[Fs2Streams[F], F])` meth Here is simple example which starts HTTP server with one route: ```scala -import cats.effect._ +import cats.effect.* import cats.effect.std.Dispatcher import io.vertx.core.Vertx import io.vertx.ext.web.Router -import sttp.tapir._ +import sttp.tapir.* import sttp.tapir.server.vertx.cats.VertxCatsServerInterpreter -import sttp.tapir.server.vertx.cats.VertxCatsServerInterpreter._ +import sttp.tapir.server.vertx.cats.VertxCatsServerInterpreter.* -object App extends IOApp { +object App extends IOApp: val responseEndpoint: PublicEndpoint[String, Unit, String, Any] = endpoint .in("response") @@ -95,7 +92,7 @@ object App extends IOApp { def handler(req: String): IO[Either[Unit, String]] = IO.pure(Right(req)) - override def run(args: List[String]): IO[ExitCode] = { + override def run(args: List[String]): IO[ExitCode] = Dispatcher[IO] .flatMap { dispatcher => Resource @@ -113,18 +110,16 @@ object App extends IOApp { }) } .use(_ => IO.never) - } -} ``` This interpreter also supports streaming using FS2 streams: ```scala -import cats.effect._ +import cats.effect.* import cats.effect.std.Dispatcher -import fs2._ +import fs2.* import sttp.capabilities.fs2.Fs2Streams -import sttp.tapir._ +import sttp.tapir.* import sttp.tapir.server.vertx.cats.VertxCatsServerInterpreter val streamedResponse = @@ -145,14 +140,14 @@ val attach = VertxCatsServerInterpreter(dispatcher).route(streamedResponse.serve Add the following dependency ```scala -"com.softwaremill.sttp.tapir" %% "tapir-vertx-server-zio" % "1.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-vertx-server-zio" % "1.10.13" ``` to use this interpreter with ZIO. Then import the object: ```scala -import sttp.tapir.server.vertx.zio.VertxZioServerInterpreter._ +import sttp.tapir.server.vertx.zio.VertxZioServerInterpreter.* ``` This object contains method `def route(e: ServerEndpoint[ZioStreams, RIO[R, *]])` which returns a function `Router => Route` that will create a route matching the endpoint definition, and with the logic attached as a handler. @@ -163,12 +158,12 @@ Here is simple example which starts HTTP server with one route: import io.vertx.core.Vertx import io.vertx.ext.web.Router import sttp.tapir.{plainBody, query} -import sttp.tapir.ztapir._ +import sttp.tapir.ztapir.* import sttp.tapir.server.vertx.zio.VertxZioServerInterpreter -import sttp.tapir.server.vertx.zio.VertxZioServerInterpreter._ -import zio._ +import sttp.tapir.server.vertx.zio.VertxZioServerInterpreter.* +import zio.* -object Short extends ZIOAppDefault { +object Short extends ZIOAppDefault: override implicit val runtime = zio.Runtime.default val responseEndpoint = @@ -179,7 +174,7 @@ object Short extends ZIOAppDefault { val attach = VertxZioServerInterpreter().route(responseEndpoint.zServerLogic { key => ZIO.succeed(key) }) - override def run = { + override def run = ZIO.scoped( ZIO .acquireRelease( @@ -196,8 +191,6 @@ object Short extends ZIOAppDefault { ZIO.attempt(server.close()).flatMap(_.asRIO).orDie } *> ZIO.never ) - } -} ``` This interpreter supports streaming using ZStreams. diff --git a/generated-doc/out/server/zio-http4s.md b/generated-doc/out/server/zio-http4s.md index 6a25225217..ef2f8ade3c 100644 --- a/generated-doc/out/server/zio-http4s.md +++ b/generated-doc/out/server/zio-http4s.md @@ -8,26 +8,26 @@ 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-zio" % "1.10.13" ``` or just add the zio-http4s integration which already depends on `tapir-zio`: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-http4s-server-zio" % "1.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-http4s-server-zio" % "1.10.13" ``` -Next, instead of the usual `import sttp.tapir._`, you should import (or extend the `ZTapir` trait, see [MyTapir](../mytapir.md)): +Next, instead of the usual `import sttp.tapir.*`, you should import (or extend the `ZTapir` trait, see [MyTapir](../other/mytapir.md)): ```scala -import sttp.tapir.ztapir._ +import sttp.tapir.ztapir.* ``` This brings into scope all of the [basic](../endpoint/basics.md) input/output descriptions, which can be used to define an endpoint. ```{note} You should have only one of these imports in your source file. Otherwise, you'll get naming conflicts. The -`import sttp.tapir.ztapir._` import is meant as a complete replacement of `import sttp.tapir._`. +`import sttp.tapir.ztapir.*` import is meant as a complete replacement of `import sttp.tapir.*`. ``` ## Server logic @@ -68,7 +68,7 @@ so that it is uniform across all endpoints, using the `.widen` method: ```scala import org.http4s.HttpRoutes -import sttp.tapir.ztapir._ +import sttp.tapir.ztapir.* import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter import zio.RIO @@ -118,7 +118,7 @@ method. This can then be added to a server builder using `withHttpWebSocketApp`, import sttp.capabilities.WebSockets import sttp.capabilities.zio.ZioStreams import sttp.tapir.{CodecFormat, PublicEndpoint} -import sttp.tapir.ztapir._ +import sttp.tapir.ztapir.* import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter import org.http4s.HttpRoutes import org.http4s.blaze.server.BlazeServerBuilder @@ -126,12 +126,12 @@ import org.http4s.server.Router import org.http4s.server.websocket.WebSocketBuilder2 import scala.concurrent.ExecutionContext import zio.{Task, Runtime, ZIO} -import zio.interop.catz._ +import zio.interop.catz.* import zio.stream.Stream def runtime: Runtime[Any] = ??? // provided by ZIOAppDefault -implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global +given ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global val wsEndpoint: PublicEndpoint[Unit, Unit, Stream[Throwable, String] => Stream[Throwable, String], ZioStreams with WebSockets] = endpoint.get.in("count").out(webSocketBody[String, CodecFormat.TextPlain, String, CodecFormat.TextPlain](ZioStreams)) @@ -162,7 +162,7 @@ import sttp.capabilities.zio.ZioStreams import sttp.model.sse.ServerSentEvent import sttp.tapir.server.http4s.ztapir.{ZHttp4sServerInterpreter, serverSentEventsBody} import sttp.tapir.PublicEndpoint -import sttp.tapir.ztapir._ +import sttp.tapir.ztapir.* import org.http4s.HttpRoutes import zio.{Task, ZIO} import zio.stream.{Stream, ZStream} diff --git a/generated-doc/out/server/ziohttp.md b/generated-doc/out/server/ziohttp.md index 16d8e85ec4..a369659b11 100644 --- a/generated-doc/out/server/ziohttp.md +++ b/generated-doc/out/server/ziohttp.md @@ -8,26 +8,26 @@ 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-zio" % "1.10.13" ``` or just add the zio-http integration which already depends on `tapir-zio`: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-zio-http-server" % "1.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-zio-http-server" % "1.10.13" ``` -Next, instead of the usual `import sttp.tapir._`, you should import (or extend the `ZTapir` trait, see [MyTapir](../mytapir.md)): +Next, instead of the usual `import sttp.tapir.*`, you should import (or extend the `ZTapir` trait, see [MyTapir](../other/mytapir.md)): ```scala -import sttp.tapir.ztapir._ +import sttp.tapir.ztapir.* ``` This brings into scope all the [basic](../endpoint/basics.md) input/output descriptions, which can be used to define an endpoint. ```{note} You should have only one of these imports in your source file. Otherwise, you'll get naming conflicts. The -`import sttp.tapir.ztapir._` import is meant as a complete replacement of `import sttp.tapir._`. +`import sttp.tapir.ztapir.*` import is meant as a complete replacement of `import sttp.tapir.*`. ``` ## Exposing endpoints @@ -41,10 +41,10 @@ example: ```scala import sttp.tapir.PublicEndpoint -import sttp.tapir.ztapir._ +import sttp.tapir.ztapir.* import sttp.tapir.server.ziohttp.ZioHttpInterpreter import zio.http.{Request, Response, Routes} -import zio._ +import zio.* def countCharacters(s: String): ZIO[Any, Nothing, Int] = ZIO.succeed(s.length) diff --git a/generated-doc/out/support.md b/generated-doc/out/support.md new file mode 100644 index 0000000000..3dbbb1b16c --- /dev/null +++ b/generated-doc/out/support.md @@ -0,0 +1,11 @@ +# Support & sponsorship + +## Sponsors + +Development and maintenance of sttp tapir is sponsored by [SoftwareMill](https://softwaremill.com), a software development and consulting company. We help clients scale their business through software. We offer services around migrating and maintaining Java and Scala projects (e.g. to Java 21, or across Scala versions), ML/AI discovery workshops, introducing developer platforms (based on Kubernetes and observability technologies), and others. Our areas of expertise include performant backends, distributed systems, machine learning and data analytics, with a focus on Java, Scala, Kafka, TypeScript and Rust. + +[![](https://files.softwaremill.com/logo/logo.png "SoftwareMill")](https://softwaremill.com) + +## Commercial Support + +We offer commercial support for sttp and related technologies, as well as development services. [Contact us](https://softwaremill.com/contact/) to learn more about our offer! diff --git a/generated-doc/out/testing.md b/generated-doc/out/testing.md index a04281f40c..f36e110d90 100644 --- a/generated-doc/out/testing.md +++ b/generated-doc/out/testing.md @@ -23,13 +23,13 @@ Tapir builds upon the `SttpBackendStub` to enable stubbing using `Endpoint`s or dependency: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-sttp-stub-server" % "1.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-sttp-stub-server" % "1.10.13" ``` -Let's assume you are using the [akka http](server/akkahttp.md) interpreter. Given the following server endpoint: +Let's assume you are using the [pekko http](server/pekkohttp.md) interpreter. Given the following server endpoint: ```scala -import sttp.tapir._ +import sttp.tapir.* import sttp.tapir.server.ServerEndpoint import scala.concurrent.Future @@ -53,11 +53,11 @@ A test which verifies how this endpoint behaves when interpreter as a server mig ```scala import org.scalatest.flatspec.AsyncFlatSpec import org.scalatest.matchers.should.Matchers -import sttp.client3._ +import sttp.client3.* import sttp.client3.testing.SttpBackendStub import sttp.tapir.server.stub.TapirStubInterpreter -class MySpec extends AsyncFlatSpec with Matchers { +class MySpec extends AsyncFlatSpec with Matchers: it should "work" in { // given val backendStub: SttpBackend[Future, Any] = TapirStubInterpreter(SttpBackendStub.asynchronousFuture) @@ -74,7 +74,6 @@ class MySpec extends AsyncFlatSpec with Matchers { // then response.map(_.body shouldBe Right("hello user123")) } -} ``` The `.backend` method creates the enriched `SttpBackendStub`, using the provided server endpoints and their @@ -85,12 +84,12 @@ Projects generated using [adopt-tapir](https://adopt-tapir.softwaremill.com) inc ### Custom interpreters Custom interpreters can be provided to the stub. For example, to test custom exception handling, we might have the -following customised akka http options: +following customised pekko http options: ```scala import sttp.tapir.server.interceptor.exception.ExceptionHandler import sttp.tapir.server.interceptor.CustomiseInterceptors -import sttp.tapir.server.akkahttp.AkkaHttpServerOptions +import sttp.tapir.server.pekkohttp.PekkoHttpServerOptions import sttp.tapir.server.model.ValuedEndpointOutput import sttp.model.StatusCode @@ -101,9 +100,9 @@ val exceptionHandler = ExceptionHandler.pure[Future](ctx => )) ) -val customOptions: CustomiseInterceptors[Future, AkkaHttpServerOptions] = { +val customOptions: CustomiseInterceptors[Future, PekkoHttpServerOptions] = { import scala.concurrent.ExecutionContext.Implicits.global - AkkaHttpServerOptions.customiseInterceptors + PekkoHttpServerOptions.customiseInterceptors .exceptionHandler(exceptionHandler) } ``` @@ -111,8 +110,7 @@ val customOptions: CustomiseInterceptors[Future, AkkaHttpServerOptions] = { Testing such an interceptor requires simulating an exception being thrown in the server logic: ```scala -class MySpec2 extends AsyncFlatSpec with Matchers { - +class MySpec2 extends AsyncFlatSpec with Matchers: it should "use my custom exception handler" in { // given val stub = TapirStubInterpreter(customOptions, SttpBackendStub.asynchronousFuture) @@ -127,7 +125,6 @@ class MySpec2 extends AsyncFlatSpec with Matchers { // then .map(_.body shouldBe Left("failed due to error")) } -} ``` Note that to provide alternate success/error outputs given a `ServerEndpoint`, the endpoint will have to be typed @@ -142,23 +139,23 @@ 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.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-sttp-stub-server" % "1.10.13" ``` And the following imports: ```scala import sttp.client3.testing.SttpBackendStub -import sttp.tapir.server.stub._ +import sttp.tapir.server.stub.* ``` Then, given the following endpoint: ```scala -import sttp.tapir._ -import sttp.tapir.generic.auto._ -import sttp.tapir.json.circe._ -import io.circe.generic.auto._ +import sttp.tapir.* +import sttp.tapir.generic.auto.* +import sttp.tapir.json.circe.* +import io.circe.generic.auto.* case class ResponseWrapper(value: Double) @@ -197,22 +194,22 @@ with [mock-server](https://www.mock-server.com/) Add the following dependency: ```scala -"com.softwaremill.sttp.tapir" %% "sttp-mock-server" % "1.10.12" +"com.softwaremill.sttp.tapir" %% "sttp-mock-server" % "1.10.13" ``` Imports: ```scala -import sttp.tapir.server.mockserver._ +import sttp.tapir.server.mockserver.* ``` Then, given the following endpoint: ```scala -import sttp.tapir._ -import sttp.tapir.generic.auto._ -import sttp.tapir.json.circe._ -import io.circe.generic.auto._ +import sttp.tapir.* +import sttp.tapir.generic.auto.* +import sttp.tapir.json.circe.* +import io.circe.generic.auto.* case class SampleIn(name: String, age: Int) @@ -268,7 +265,7 @@ result == out To use, add the following dependency: ```scala -"com.softwaremill.sttp.tapir" %% "tapir-testing" % "1.10.12" +"com.softwaremill.sttp.tapir" %% "tapir-testing" % "1.10.13" ``` ### Shadowed endpoints @@ -360,4 +357,4 @@ Results in: ```scala result2.toString // res5: String = "Set(An endpoint OPTIONS GET /a /b /c -> -/- have multiple method definitions: List(OPTIONS, GET))" -``` \ No newline at end of file +``` diff --git a/generated-doc/out/tutorials/01_hello_world.md b/generated-doc/out/tutorials/01_hello_world.md index dd27966074..1346394387 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.10.12 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.10.12 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.10.13 ``` ## 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.10.12 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.10.12 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.10.13 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.10.12 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.10.12 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.10.13 import sttp.tapir.* @@ -98,8 +98,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.10.12 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.10.12 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.10.13 import sttp.tapir.* @@ -124,8 +124,8 @@ will be sent as a response: {emphasize-lines="12"} ```scala -//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.12 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.10.12 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.10.13 import sttp.tapir.* @@ -153,8 +153,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.10.12 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.10.12 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.10.13 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 5e8da7a2b7..84cf958536 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.10.12 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.10.13 ``` 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.10.12 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.10.12 -//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.10.12 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.10.13 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.10.12 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.10.12 -//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.10.12 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.10.13 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 cfe4175582..124fa70ba7 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.10.12 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 //> 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.10.12 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.10.12 -//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.10.12 -//> using dep com.softwaremill.sttp.tapir::tapir-jsoniter-scala:1.10.12 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-jsoniter-scala:1.10.13 //> 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 4404103b24..2d45e48c4e 100644 --- a/generated-doc/out/tutorials/04_errors.md +++ b/generated-doc/out/tutorials/04_errors.md @@ -1,5 +1,9 @@ # 4. Error handling +```{note} +The tutorial is also available [as a video](https://www.youtube.com/watch?v=iXGJsk4_2Dg). +``` + Many things can go wrong: that's why error handling is often the centerpiece of software libraries. We got a glimpse of one of Tapir's components when it comes to error handling when we discussed [adding OpenAPI documentation](02_openapi_docs.md). In this tutorial, we'll investigate Tapir's approach to error @@ -36,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.10.12 -//> using dep com.softwaremill.sttp.tapir::tapir-jsoniter-scala:1.10.12 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-jsoniter-scala:1.10.13 //> using dep com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros:2.30.1 import com.github.plokhotnyuk.jsoniter_scala.macros.* @@ -72,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.10.12 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.10.12 -//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.10.12 -//> using dep com.softwaremill.sttp.tapir::tapir-jsoniter-scala:1.10.12 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-jsoniter-scala:1.10.13 //> using dep com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros:2.30.1 import com.github.plokhotnyuk.jsoniter_scala.macros.* @@ -148,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.10.12 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.10.12 -//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.10.12 -//> using dep com.softwaremill.sttp.tapir::tapir-jsoniter-scala:1.10.12 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-jsoniter-scala:1.10.13 //> 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 d18a1cedc4..2a765f58c2 100644 --- a/generated-doc/out/tutorials/05_multiple_inputs_outputs.md +++ b/generated-doc/out/tutorials/05_multiple_inputs_outputs.md @@ -1,5 +1,9 @@ # 5. Multiple inputs & outputs +```{note} +The tutorial is also available [as a video](https://www.youtube.com/watch?v=rJAo9yZfr9k). +``` + In the tutorials so far we've seen how to use endpoints which have a single input and a single output, optionally with an additional single error output. However, most commonly you'll have multiple inputs and outputs. This can include multiple path, query parameters and headers, accompanied by a body as inputs, along with multiple output @@ -20,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.10.12 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 import sttp.tapir.* @@ -57,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.10.12 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.10.12 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.10.13 import sttp.tapir.* import sttp.tapir.server.netty.sync.NettySyncServer @@ -143,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.10.12 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.10.12 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.10.13 import sttp.tapir.* import sttp.tapir.server.netty.sync.NettySyncServer @@ -193,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.10.12 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.10.12 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.10.13 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 32188cb02c..ddcd4a7851 100644 --- a/generated-doc/out/tutorials/06_error_variants.md +++ b/generated-doc/out/tutorials/06_error_variants.md @@ -1,5 +1,9 @@ # 6. Error variants +```{note} +The tutorial is also available [as a video](https://www.youtube.com/watch?v=w2ZL3WvhBZ8). +``` + Quite often, there's more than one thing that might go wrong. On the other hand, success can also have many facets. In the previous tutorials we've seen that Tapir includes built-in support for differentiating between successful and @@ -79,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.10.12 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 import sttp.model.{HeaderNames, StatusCode} import sttp.tapir.* @@ -102,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.10.12 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 import sttp.model.{HeaderNames, StatusCode} import sttp.tapir.* @@ -134,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.10.12 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 import sttp.model.{HeaderNames, StatusCode} import sttp.tapir.* @@ -171,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.10.12 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 import sttp.model.{HeaderNames, StatusCode} import sttp.tapir.* @@ -193,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.10.12 -//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.10.12 -//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.10.12 +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.10.13 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 new file mode 100644 index 0000000000..7fae95a07b --- /dev/null +++ b/generated-doc/out/tutorials/07_cats_effect.md @@ -0,0 +1,247 @@ +# 7. Integration with cats-effect & http4s + +[cats-effect](https://github.com/typelevel/cats-effect) is one of the most popular functional effect systems for Scala +(probably also one of the top ones when it comes to pure functional programming in general). So far we've used Tapir in +combination with so-called "direct style", where the server logic is expressed using synchronous, blocking code. + +However, one of Tapir's main strengths is that it integrates with virtually every Scala stack out there. This includes, +first and foremost, cats-effect. Let's see how we can combine the two libraries together. + +## Describing an endpoint + +We'll assume that you are familiar with what's described in the previous tutorials, especially [](01_hello_world.md). +The good news is that most of what's described there applies 1-1 to our scenario, when we want to use cats-effect. The +process of describing endpoints is identical, regardless of what programming style, Scala stack of effect library you +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.10.13 + +import sttp.tapir.* + +@main def helloWorldTapir(): Unit = + val helloWorldEndpoint = endpoint + .get + .in("hello" / "world") + .in(query[String]("name")) + .out(stringBody) +``` + +```{note} +As a side note, while our previous examples required Java 21+, as they leveraged virtual threads under the hood, the +cats-effect version will work with Java 11+. +``` + +## Server-side logic + +The crucial difference when using Tapir+cats-effect, as compared to the "direct" version is in the way the server +logic is provided. The server logic does, most probably, involve side effects. Typically, this might be querying the +database, writing to Kafka, or invoking other endpoints (though in our example, here we'll constrain ourselves to good +old `println`s). Hence, any operations that the server logic performs should be captured using the `IO` monad. + +That is, given an endpoint with inputs of type `I` and error/success outputs of type `E` and `O`, the shape of the +server logic function should be `I => IO[Either[E, O]]`. In other words: given the input parameters `I`, extracted from +the request, the server logic should return a description of a computation, yielding either error `E` or success `O` +outputs, which will then be mapped to the HTTP response. + +To combine an endpoint description with the server logic, we need to use the `.serverLogic` method. While not always +required, as the type parameter is usually inferred correctly, it's nevertheless beneficial to provide the effect type +parameter explicitly, using `.serverLogic[IO]` in our case: + +{emphasize-lines="2, 4, 12-14"} +```scala +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-cats:1.10.13 + +import cats.effect.IO +import sttp.tapir.* + +@main def helloWorldTapir(): Unit = + val helloWorldEndpoint = endpoint.get + .in("hello" / "world") + .in(query[String]("name")) + .out(stringBody) + .serverLogic[IO](name => IO + .println(s"Saying hello to: $name") + .flatMap(_ => IO.pure(Right(s"Hello, $name!")))) +``` + +The server-side logic consists of printing a message to the server's logs (`IO.println(...)`), followed by returning +a pure value - successful result. The `s"Hello, $name"` string that will be mapped to the response needs to be first +wrapped with a `Right` (as we want to use the successful outputs), and then lifted to an `IO` computation description +using `IO.pure`. That way, we obtain a value of type `IO[Either[Unit, String]]`, as required by the endpoint +description. + +## http4s integration + +So far we've described the shape of the endpoint, and coupled it with a function implementing the server logic, with +a matching signature. What we still need to do is to expose the endpoint using a server. + +The server must be "cats-effect-aware" - that is, it must need to know how to deal with server-side logic, which is +expressed in terms of `IO` computation descriptions. So far we've been using the `NettySyncServer`, however here it +won't be useful. Attempting to use it with our endpoint description won't compile, as there would be a mismatch on the +type used to represent effects (`Identity` vs `IO`). + +Instead, we need to use a different server. Tapir provides a couple of choices (which might be useful depending on what +you're already using in your project), but the most popular option in case of cats-effect is the +[http4s](https://http4s.org) server. That's what we're going to do as well: through a Tapir-http4s integration, called +a server interpreter. + +We've already introduced interpreters in the tutorial [](02_openapi_docs.md). In this particular case, the http4s +server interpreter will convert our endpoint description+server logic into a `HttpRoutes[IO]` type. This type is +the representation of HTTP routes that is understandable by the http4s API, and which can be used to expose a server +to the outside world. + +The conversion process is an almost-one-liner (if it wasn't for line length limit!): + +{emphasize-lines="2, 5, 7, 18-19"} +```scala +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.10.13 + +import cats.effect.IO +import org.http4s.HttpRoutes +import sttp.tapir.* +import sttp.tapir.server.http4s.Http4sServerInterpreter + +@main def helloWorldTapir(): Unit = + val helloWorldEndpoint = endpoint.get + .in("hello" / "world") + .in(query[String]("name")) + .out(stringBody) + .serverLogic[IO](name => IO + .println(s"Saying hello to: $name") + .flatMap(_ => IO.pure(Right(s"Hello, $name!")))) + + val helloWorldRoutes: HttpRoutes[IO] = Http4sServerInterpreter[IO]() + .toRoutes(helloWorldEndpoint) +``` + +## Exposing the server + +As a final step, we need to expose the routes to the outside world. If you've ever used http4s, the following is fairly +standard code to start a server and handle requests until the application is interrupted or killed: + +{emphasize-lines="3, 5, 7, 8, 12, 24-30"} +```scala +//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.10.13 +//> using dep org.http4s::http4s-blaze-server:0.23.16 + +import cats.effect.{ExitCode, IO, IOApp} +import org.http4s.HttpRoutes +import org.http4s.blaze.server.BlazeServerBuilder +import org.http4s.server.Router +import sttp.tapir.* +import sttp.tapir.server.http4s.Http4sServerInterpreter + +object HelloWorldTapir extends IOApp: + val helloWorldEndpoint = endpoint.get + .in("hello" / "world") + .in(query[String]("name")) + .out(stringBody) + .serverLogic[IO](name => IO + .println(s"Saying hello to: $name") + .flatMap(_ => IO.pure(Right(s"Hello, $name!")))) + + val helloWorldRoutes: HttpRoutes[IO] = Http4sServerInterpreter[IO]() + .toRoutes(helloWorldEndpoint) + + override def run(args: List[String]): IO[ExitCode] = + BlazeServerBuilder[IO] + .bindHttp(8080, "localhost") + .withHttpApp(Router("/" -> helloWorldRoutes).orNotFound) + .resource + .use(_ => IO.never) + .as(ExitCode.Success) +``` + +First of all, you might notice that instead of the `@main` method, we are extending the `IOApp` trait. This is needed, +because not only our endpoint's server logic is expressed using `IO`, but the entire process of starting the server +and handling requests is described as an `IO` computation. Hence, we need to start the application in an `IO`-aware way: +the `IOApp` will handle evaluating the `IO` description and actually running the code. + +Secondly, with http4s we need to use a specific server implementation (http4s itself is only an API to define endpoints - +kind of a middle-man between Tapir and low-level networking code). We can choose from `blaze` and `ember` servers, here +we're using the `blaze` one, which is reflected in the additional dependency and the server configuration constructor: +`BlazeServerBuilder`. + +Finally, we've got the `run` method implementation, which attaches our interpreted route to the root context `/` and +exposes the server on `localhost:8080`. + +```{note} +Note that you could also attach other, non-Tapir-managed routes to the same http4s application. Tapir-interpreted +`HttpRoutes[IO]` can co-exist with routes defined in any other way. +``` + +## Adding documentation + +As a final touch, let's expose documentation using the Swagger UI, just as we did before using the Netty server. +The base process is the same: we first need to call the `SwaggerInterpreter` providing the list of endpoints, for which +documentation should be generated. However, this time we'll provide the `IO` type constructor as the type parameter. + +That way, the server logic implementing the behavior of the swagger endpoints (such as reading the .js/.css/.html +resources) will be expressed in terms of `IO`, and we'll be able to convert them later to http4s routes. And that's +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.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.10.13 +//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.10.13 +//> using dep org.http4s::http4s-blaze-server:0.23.16 + +import cats.effect.{ExitCode, IO, IOApp} +import cats.syntax.all.* +import org.http4s.HttpRoutes +import org.http4s.blaze.server.BlazeServerBuilder +import org.http4s.server.Router +import sttp.tapir.* +import sttp.tapir.server.http4s.Http4sServerInterpreter +import sttp.tapir.swagger.bundle.SwaggerInterpreter + +object HelloWorldTapir extends IOApp: + val helloWorldEndpoint = endpoint.get + .in("hello" / "world") + .in(query[String]("name")) + .out(stringBody) + .serverLogic[IO](name => IO + .println(s"Saying hello to: $name") + .flatMap(_ => IO.pure(Right(s"Hello, $name!")))) + + val helloWorldRoutes: HttpRoutes[IO] = Http4sServerInterpreter[IO]() + .toRoutes(helloWorldEndpoint) + + val swaggerEndpoints = SwaggerInterpreter() + .fromServerEndpoints[IO](List(helloWorldEndpoint), "My App", "1.0") + + val swaggerRoutes: HttpRoutes[IO] = Http4sServerInterpreter[IO]().toRoutes(swaggerEndpoints) + + val allRoutes: HttpRoutes[IO] = helloWorldRoutes <+> swaggerRoutes + + override def run(args: List[String]): IO[ExitCode] = + BlazeServerBuilder[IO] + .bindHttp(8080, "localhost") + .withHttpApp(Router("/" -> allRoutes).orNotFound) + .resource + .use(_ => IO.never) + .as(ExitCode.Success) +``` + +Hence, we first generate endpoint descriptions, which correspond to exposing the Swagger UI (containing the generated +OpenAPI yaml for our `/hello/world` endpoint), which use `IO` to express their server logic. Then, we interpret those +endpoints as `HttpRoutes[IO]`, which we can expose using http4's blaze server. + +## Other concepts covered so far + +We can use JSON integration, error outputs, status codes, and any other Tapir features in the same way as we did so +far with the "synchronous" server! The endpoints are described in the same way, the only thing that changes is how the +server logic is provided. + +## Further reading + +* [Netty-cats interpreter](../server/netty.md) +* [Armeria-cats interpreter](../server/armeria.md) +* [Integration with cats datatypes](../endpoint/customtypes.md) 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 f86e7bbb23..30d6434497 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 @@ -11,8 +11,8 @@ libraryDependencies ++= Seq( "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.4", - "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % "2.30.4" % "compile-internal", + "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % "2.30.7", + "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % "2.30.7" % "compile-internal", "org.scalatest" %% "scalatest" % "3.2.19" % Test, "com.softwaremill.sttp.tapir" %% "tapir-sttp-stub-server" % "1.10.0" % Test ) diff --git a/project/FileUtils.scala b/project/FileUtils.scala new file mode 100644 index 0000000000..863e77d4ab --- /dev/null +++ b/project/FileUtils.scala @@ -0,0 +1,22 @@ +import java.io.File +import java.nio.file.{FileVisitResult, Files, Path, SimpleFileVisitor} +import java.nio.file.attribute.BasicFileAttributes + +object FileUtils { + def listScalaFiles(basePath: File): Seq[Path] = { + val dirPath = basePath.toPath + var result = Vector.empty[Path] + + val fileVisitor = new SimpleFileVisitor[Path] { + override def visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult = { + if (file.toString.endsWith(".scala")) { + result = result :+ file + } + FileVisitResult.CONTINUE + } + } + + Files.walkFileTree(dirPath, fileVisitor) + result + } +} diff --git a/project/GenerateListOfExamples.scala b/project/GenerateListOfExamples.scala new file mode 100644 index 0000000000..da3f8b47fb --- /dev/null +++ b/project/GenerateListOfExamples.scala @@ -0,0 +1,97 @@ +import java.io.{File, PrintWriter} +import sbt.Logger + +import java.nio.file.Path +import scala.io.Source + +/** Generates the generated-doc/out/includes/examples_list.md file, basing on the metadata included in the source of the examples. + */ +object GenerateListOfExamples { + private val MetadataPattern = """// \{(.+?)}: (.+)""".r + private val LinkBase = "https://github.com/softwaremill/tapir/tree/master" + + private case class Example(cat: String, otherMetadata: Map[String, String], description: String, path: Path) + + def apply(log: Logger, rootBasePath: File): Unit = { + log.info(s"[GenerateListOfExamples] Base path: $rootBasePath") + + // from each of those examples, we need to extract the metadata + val examples = FileUtils.listScalaFiles(rootBasePath.toPath.resolve("examples/src/main").toFile) + + val parsedExamples: Seq[Option[Example]] = for (example <- examples) yield { + val first = firstLine(example) + if (first.startsWith("// {")) { + first match { + case MetadataPattern(metadataStr, description) => + val metadata = metadataStr.trim + .split(";") + .map { entry => + val Array(key, value) = entry.split("=") + key.trim -> value.trim + } + .toMap + + Some(Example(metadata("cat"), metadata - "cat", description, example)) + } + } else { + log.warn(s"[GenerateListOfExamples] Skipping $example as it doesn't start with the required prefix") + None + } + } + + val examplesByCategory = parsedExamples.flatten.groupBy(_.cat) + val renderedCategories = examplesByCategory.toList + // we want the "Hello, World!" category to come first + .sortBy { case (k, _) => if (k == "Hello, World!") "00" else k.toLowerCase() } + .map { case (category, examples) => + val renderedExamplesList = examples + .sortBy(_.description) + // rendering a single line with the example's description & metadata + .map { example => + val relativeLink = relativePath(example.path.toFile, rootBasePath) + val tags = example.otherMetadata.toList + .sortBy(_._1) + .map { case (key, value) => + s"""$value""" + } + .mkString(" ") + + s"""* [${example.description}]($LinkBase/$relativeLink) $tags""" + } + // combining all examples in category + .mkString("\n") + + s"""## $category + | + |$renderedExamplesList""".stripMargin + } + // separating categories + .mkString("\n\n") + + // writing the result + val targetPath = rootBasePath.toPath.resolve("generated-doc/out/includes/examples_list.md") + ensureExists(targetPath.getParent.toFile) + log.info(s"[GenerateListOfExamples] Writing rendered categories to $targetPath") + + val writer = new PrintWriter(targetPath.toFile) + try writer.write(renderedCategories) + finally writer.close() + } + + private def firstLine(p: Path): String = { + val source = Source.fromFile(p.toUri) + try source.getLines().next() + finally source.close() + } + + private def relativePath(file: File, relativeTo: File): String = { + val p1 = file.getAbsolutePath + val p2 = relativeTo.getAbsolutePath + if (!p1.startsWith(p2)) throw new IllegalArgumentException(s"$file is not relative to $relativeTo!") + else p1.substring(p2.length) + } + + private def ensureExists(dir: File): Unit = if (!dir.exists() && !dir.mkdirs()) { + throw new IllegalStateException("Cannot create directory: " + dir) + } +} diff --git a/project/VerifyExamplesCompileUsingScalaCli.scala b/project/VerifyExamplesCompileUsingScalaCli.scala new file mode 100644 index 0000000000..54bd6198d1 --- /dev/null +++ b/project/VerifyExamplesCompileUsingScalaCli.scala @@ -0,0 +1,24 @@ +import java.io.File +import sbt.Logger +import scala.sys.process.{Process, ProcessLogger} + +object VerifyExamplesCompileUsingScalaCli { + def apply(log: Logger, examplesSrcPath: File): Unit = { + val examples = FileUtils.listScalaFiles(examplesSrcPath) + log.info(s"Found ${examples.size} examples") + + for (example <- examples) { + log.info(s"Compiling: $example") + val errorOutput = new StringBuilder + val logger = ProcessLogger((o: String) => (), (e: String) => errorOutput.append(e + "\n")) + try { + val result = Process(List("scala-cli", "compile", example.toFile.getAbsolutePath), examplesSrcPath).!(logger) + if (result != 0) { + throw new Exception(s"""Compiling $example failed.\n$errorOutput""".stripMargin) + } + } finally { + Process(List("scala-cli", "clean", example.toFile.getAbsolutePath), examplesSrcPath).! + } + } + } +} diff --git a/project/Versions.scala b/project/Versions.scala index d1a2419a20..6fc1a38e8b 100644 --- a/project/Versions.scala +++ b/project/Versions.scala @@ -25,7 +25,7 @@ object Versions { val json4s = "4.0.7" val metrics4Scala = "4.2.9" val nettyReactiveStreams = "3.0.2" - val ox = "0.2.1" + val ox = "0.3.1" val reactiveStreams = "1.0.4" val sprayJson = "1.3.6" val scalaCheck = "1.18.0" @@ -35,14 +35,14 @@ object Versions { val refined = "0.11.2" val iron = "2.6.0" val enumeratum = "1.7.4" - val zio = "2.1.5" + val zio = "2.1.6" val zioHttp = "3.0.0-RC8" val zioInteropCats = "23.0.0.8" val zioInteropReactiveStreams = "2.0.2" val zioJson = "0.7.1" - val playClient = "3.0.4" + val playClient = "3.0.5" val playServer = "3.0.4" - val play29Client = "2.2.8" + val play29Client = "2.2.9" val play29Server = "2.9.4" val tethys = "0.28.4" val vertx = "4.5.8" @@ -54,7 +54,7 @@ object Versions { val monixNewtype = "0.2.3" val zioPrelude = "1.0.0-RC27" val awsLambdaInterface = "2.5.1" - val armeria = "1.29.1" + val armeria = "1.29.2" val scalaJava8Compat = "1.0.2" val scalaCollectionCompat = "2.12.0" val fs2 = "3.10.2" diff --git a/server/akka-http-server/src/test/scala/sttp/tapir/server/akkahttp/AkkaHttpTestServerInterpreter.scala b/server/akka-http-server/src/test/scala/sttp/tapir/server/akkahttp/AkkaHttpTestServerInterpreter.scala index 19fd6f5e46..801aebed98 100644 --- a/server/akka-http-server/src/test/scala/sttp/tapir/server/akkahttp/AkkaHttpTestServerInterpreter.scala +++ b/server/akka-http-server/src/test/scala/sttp/tapir/server/akkahttp/AkkaHttpTestServerInterpreter.scala @@ -23,22 +23,14 @@ class AkkaHttpTestServerInterpreter(implicit actorSystem: ActorSystem) AkkaHttpServerInterpreter(serverOptions).toRoute(es) } - override def serverWithStop( + override def server( routes: NonEmptyList[Route], gracefulShutdownTimeout: Option[FiniteDuration] - ): Resource[IO, (Port, KillSwitch)] = { + ): Resource[IO, Port] = { val bind = IO.fromFuture(IO(Http().newServerAt("localhost", 0).bind(concat(routes.toList: _*)))) Resource - .make( - bind.map(b => - ( - b.localAddress.getPort, - IO.fromFuture(IO(b.terminate(gracefulShutdownTimeout.getOrElse(50.millis)))).void - ) - ) - ) { case (_, release) => - release - } + .make(bind)(server => IO.fromFuture(IO(server.terminate(gracefulShutdownTimeout.getOrElse(50.millis)))).void) + .map(_.localAddress.getPort) } } diff --git a/server/armeria-server/src/test/scala/sttp/tapir/server/armeria/ArmeriaTestServerInterpreter.scala b/server/armeria-server/src/test/scala/sttp/tapir/server/armeria/ArmeriaTestServerInterpreter.scala index 4fab031ca9..296f4d1417 100644 --- a/server/armeria-server/src/test/scala/sttp/tapir/server/armeria/ArmeriaTestServerInterpreter.scala +++ b/server/armeria-server/src/test/scala/sttp/tapir/server/armeria/ArmeriaTestServerInterpreter.scala @@ -11,10 +11,10 @@ import scala.concurrent.duration._ trait ArmeriaTestServerInterpreter[S <: Streams[S], F[_], OPTIONS] extends TestServerInterpreter[F, S, OPTIONS, TapirService[S, F]] { - override def serverWithStop( + override def server( routes: NonEmptyList[TapirService[S, F]], gracefulShutdownTimeout: Option[FiniteDuration] - ): Resource[IO, (Port, KillSwitch)] = { + ): Resource[IO, Port] = { val (quietPeriodMs, totalDeadlineMs) = gracefulShutdownTimeout .map(t => (t.toMillis, t.toMillis + 50)) .getOrElse((0L, 0L)) @@ -31,19 +31,11 @@ trait ArmeriaTestServerInterpreter[S <: Streams[S], F[_], OPTIONS] extends TestS server.start().thenApply[Server](_ => server) } ) + // Ignore future returned by stop() for fast test iterations. + // Armeria server wait for 2 seconds by default to let the boss group gracefully finish all remaining + // tasks in the queue. Even if graceful shutdown timeouts are set to 0. Resource - .make( - bind.map(b => - ( - b.activeLocalPort(), - // Ignore returned future for fast test iterations. - // Armeria server wait for 2 seconds by default to let the boss group gracefully finish all remaining - // tasks in the queue. Even if graceful shutdown timeouts are set to 0. - IO { val _ = b.stop() } - ) - ) - ) { case (_, release) => - release - } + .make(bind)(s => IO.blocking { val _ = s.stop() }) + .map(_.activeLocalPort()) } } diff --git a/server/finatra-server/src/test/scala/sttp/tapir/server/finatra/FinatraTestServerInterpreter.scala b/server/finatra-server/src/test/scala/sttp/tapir/server/finatra/FinatraTestServerInterpreter.scala index 1cae8a25db..77e486692b 100644 --- a/server/finatra-server/src/test/scala/sttp/tapir/server/finatra/FinatraTestServerInterpreter.scala +++ b/server/finatra-server/src/test/scala/sttp/tapir/server/finatra/FinatraTestServerInterpreter.scala @@ -20,17 +20,11 @@ class FinatraTestServerInterpreter extends TestServerInterpreter[Future, Any, Fi es.map(interpreter.toRoute).last } - override def serverWithStop( + override def server( routes: NonEmptyList[FinatraRoute], gracefulShutdownTimeout: Option[FiniteDuration] = None - ): Resource[IO, (Port, KillSwitch)] = FinatraTestServerInterpreter.serverWithStop(routes, gracefulShutdownTimeout) -} + ): Resource[IO, Port] = { -object FinatraTestServerInterpreter { - def serverWithStop( - routes: NonEmptyList[FinatraRoute], - gracefulShutdownTimeout: Option[FiniteDuration] - ): Resource[IO, (Port, KillSwitch)] = { def waitUntilHealthy(s: EmbeddedHttpServer, count: Int): IO[EmbeddedHttpServer] = if (s.isHealthy) IO.pure(s) else if (count > 1000) IO.raiseError(new IllegalStateException("Server unhealthy")) @@ -65,15 +59,7 @@ object FinatraTestServerInterpreter { }.flatMap(waitUntilHealthy(_, 0)) Resource - .make( - bind.map(server => - ( - server.httpExternalPort(), - IO { server.close(Duration.fromMilliseconds(gracefulShutdownTimeout.map(_.toMillis).getOrElse(50))) } - ) - ) - ) { case (_, release) => - release - } + .make(bind)(server => IO.blocking(server.close(Duration.fromMilliseconds(gracefulShutdownTimeout.map(_.toMillis).getOrElse(50))))) + .map(_.httpExternalPort()) } } diff --git a/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sTestServerInterpreter.scala b/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sTestServerInterpreter.scala index 304f143515..a5852ce240 100644 --- a/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sTestServerInterpreter.scala +++ b/server/http4s-server/src/test/scala/sttp/tapir/server/http4s/Http4sTestServerInterpreter.scala @@ -28,23 +28,18 @@ class Http4sTestServerInterpreter extends TestServerInterpreter[IO, Fs2Streams[I Http4sServerInterpreter(serverOptions).toWebSocketRoutes(es) } - override def serverWithStop( + override def server( routes: NonEmptyList[Routes], gracefulShutdownTimeout: Option[FiniteDuration] - ): Resource[IO, (Port, KillSwitch)] = { + ): Resource[IO, Port] = { val service: WebSocketBuilder2[IO] => HttpApp[IO] = wsb => routes.map(_.apply(wsb)).reduceK.orNotFound - Resource.make( - BlazeServerBuilder[IO] - .withExecutionContext(ExecutionContext.global) - .bindHttp(0, "localhost") - .withHttpWebSocketApp(service) - .resource - .allocated - .map { case (server, release) => // Blaze has no graceful shutdown support https://github.com/http4s/blaze/issues/676 - (server.address.getPort(), release) - } - ) { case (_, release) => release } + BlazeServerBuilder[IO] + .withExecutionContext(ExecutionContext.global) + .bindHttp(0, "localhost") + .withHttpWebSocketApp(service) + .resource + .map(_.address.getPort()) } } diff --git a/server/http4s-server/zio/src/test/scala/sttp/tapir/server/http4s/ztapir/ZHttp4sTestServerInterpreter.scala b/server/http4s-server/zio/src/test/scala/sttp/tapir/server/http4s/ztapir/ZHttp4sTestServerInterpreter.scala index 23c847f7a5..04fb08dd45 100644 --- a/server/http4s-server/zio/src/test/scala/sttp/tapir/server/http4s/ztapir/ZHttp4sTestServerInterpreter.scala +++ b/server/http4s-server/zio/src/test/scala/sttp/tapir/server/http4s/ztapir/ZHttp4sTestServerInterpreter.scala @@ -2,6 +2,7 @@ package sttp.tapir.server.http4s.ztapir import cats.data.NonEmptyList import cats.effect.{IO, Resource} +import cats._ import cats.syntax.all._ import org.http4s.blaze.server.BlazeServerBuilder import org.http4s.server.websocket.WebSocketBuilder2 @@ -15,7 +16,6 @@ import sttp.tapir.tests._ import sttp.tapir.ztapir.ZServerEndpoint import zio.{Runtime, Task, Unsafe} import zio.interop.catz._ -import zio.interop.catz.implicits._ import scala.concurrent.ExecutionContext import scala.concurrent.duration.FiniteDuration @@ -34,28 +34,22 @@ class ZHttp4sTestServerInterpreter extends TestServerInterpreter[Task, ZioStream ZHttp4sServerInterpreter(serverOptions).fromWebSocket(es).toRoutes } - override def serverWithStop( + override def server( routes: NonEmptyList[Routes], gracefulShutdownTimeout: Option[FiniteDuration] - ): Resource[IO, (Port, KillSwitch)] = { + ): Resource[IO, Port] = { val service: WebSocketBuilder2[Task] => HttpApp[Task] = wsb => routes.map(_.apply(wsb)).reduceK.orNotFound - val serverResource = BlazeServerBuilder[Task] + BlazeServerBuilder[Task] .withExecutionContext(ExecutionContext.global) .bindHttp(0, "localhost") .withHttpWebSocketApp(service) .resource .map(_.address.getPort) - - // Converting a zio.RIO-resource to an cats.IO-resource - val runtime = implicitly[zio.Runtime[Any]] - Resource - .eval(IO.fromFuture(IO(Unsafe.unsafe(implicit u => Runtime.default.unsafe.runToFuture(serverResource.allocated))))) - .flatMap { case (port, release) => // Blaze has no graceful shutdown support https://github.com/http4s/blaze/issues/676 - Resource.make(IO.pure((port, IO.fromFuture(IO(Unsafe.unsafe(implicit u => Runtime.default.unsafe.runToFuture(release))))))) { - case (_, release) => release - } - } + .mapK(new ~>[Task, IO] { + // Converting a ZIO effect to an Cats Effect IO effect + def apply[B](fa: Task[B]): IO[B] = IO.fromFuture(Unsafe.unsafe(implicit u => IO(Runtime.default.unsafe.runToFuture(fa)))) + }) } } diff --git a/server/jdkhttp-server/src/test/scala/sttp/tapir/server/jdkhttp/JdkHttpTestServerInterpreter.scala b/server/jdkhttp-server/src/test/scala/sttp/tapir/server/jdkhttp/JdkHttpTestServerInterpreter.scala index d26881b7a2..3581622d3a 100644 --- a/server/jdkhttp-server/src/test/scala/sttp/tapir/server/jdkhttp/JdkHttpTestServerInterpreter.scala +++ b/server/jdkhttp-server/src/test/scala/sttp/tapir/server/jdkhttp/JdkHttpTestServerInterpreter.scala @@ -18,10 +18,10 @@ class JdkHttpTestServerInterpreter() extends TestServerInterpreter[Identity, Any JdkHttpServerInterpreter(serverOptions).toHandler(es) } - override def serverWithStop( + override def server( routes: NonEmptyList[HttpHandler], gracefulShutdownTimeout: Option[FiniteDuration] - ): Resource[IO, (Port, KillSwitch)] = { + ): Resource[IO, Port] = { val server = IO.blocking { val server = HttpServer.create(new InetSocketAddress(0), 0) @@ -48,8 +48,7 @@ class JdkHttpTestServerInterpreter() extends TestServerInterpreter[Identity, Any } Resource - .make(server.map(s => (s.getAddress.getPort, IO.blocking(s.stop(gracefulShutdownTimeout.map(_.toSeconds.toInt).getOrElse(0)))))) { - case (_, release) => release - } + .make(server)(s => IO.blocking(s.stop(gracefulShutdownTimeout.map(_.toSeconds.toInt).getOrElse(0)))) + .map(_.getAddress.getPort) } } diff --git a/server/netty-server/cats/src/test/scala/sttp/tapir/server/netty/cats/NettyCatsTestServerInterpreter.scala b/server/netty-server/cats/src/test/scala/sttp/tapir/server/netty/cats/NettyCatsTestServerInterpreter.scala index 9334504a2f..a4d4ea6a8b 100644 --- a/server/netty-server/cats/src/test/scala/sttp/tapir/server/netty/cats/NettyCatsTestServerInterpreter.scala +++ b/server/netty-server/cats/src/test/scala/sttp/tapir/server/netty/cats/NettyCatsTestServerInterpreter.scala @@ -21,10 +21,10 @@ class NettyCatsTestServerInterpreter(eventLoopGroup: NioEventLoopGroup, dispatch NettyCatsServerInterpreter(serverOptions).toRoute(es) } - override def serverWithStop( + override def server( routes: NonEmptyList[Route[IO]], gracefulShutdownTimeout: Option[FiniteDuration] = None - ): Resource[IO, (Port, KillSwitch)] = { + ): Resource[IO, Port] = { val config = NettyConfig.default .eventLoopGroup(eventLoopGroup) .randomPort @@ -36,6 +36,7 @@ class NettyCatsTestServerInterpreter(eventLoopGroup: NioEventLoopGroup, dispatch val bind: IO[NettyCatsServerBinding[IO]] = NettyCatsServer(options, customizedConfig).addRoutes(routes.toList).start() Resource - .make(bind.map(b => (b.port, b.stop()))) { case (_, release) => release } + .make(bind)(_.stop()) + .map(_.port) } } diff --git a/server/netty-server/src/test/scala/sttp/tapir/server/netty/NettyFutureTestServerInterpreter.scala b/server/netty-server/src/test/scala/sttp/tapir/server/netty/NettyFutureTestServerInterpreter.scala index 7eb0867ff1..2cc542f7a8 100644 --- a/server/netty-server/src/test/scala/sttp/tapir/server/netty/NettyFutureTestServerInterpreter.scala +++ b/server/netty-server/src/test/scala/sttp/tapir/server/netty/NettyFutureTestServerInterpreter.scala @@ -19,10 +19,10 @@ class NettyFutureTestServerInterpreter(eventLoopGroup: NioEventLoopGroup)(implic NettyFutureServerInterpreter(serverOptions).toRoute(es) } - override def serverWithStop( + override def server( routes: NonEmptyList[FutureRoute], gracefulShutdownTimeout: Option[FiniteDuration] = None - ): Resource[IO, (Port, KillSwitch)] = { + ): Resource[IO, Port] = { val config = NettyConfig.default .eventLoopGroup(eventLoopGroup) @@ -34,6 +34,7 @@ class NettyFutureTestServerInterpreter(eventLoopGroup: NioEventLoopGroup)(implic val bind = IO.fromFuture(IO.delay(NettyFutureServer(options, customizedConfig).addRoutes(routes.toList).start())) Resource - .make(bind.map(b => (b.port, IO.fromFuture(IO.delay(b.stop()))))) { case (_, release) => release } + .make(bind)(server => IO.fromFuture(IO.delay(server.stop()))) + .map(_.port) } } 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 fecdd2265d..71d98dd91d 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 @@ -75,5 +75,5 @@ private[sync] object OxSourceWebSocketProcessor: case _: WebSocketFrame.Close => false case _ => true }, - includeFailed = passAlongCloseFrame + includeFirstFailing = passAlongCloseFrame ) diff --git a/server/netty-server/sync/src/test/scala/sttp/tapir/server/netty/sync/NettySyncTestServerInterpreter.scala b/server/netty-server/sync/src/test/scala/sttp/tapir/server/netty/sync/NettySyncTestServerInterpreter.scala index 28b507cdf3..0e9155807c 100644 --- a/server/netty-server/sync/src/test/scala/sttp/tapir/server/netty/sync/NettySyncTestServerInterpreter.scala +++ b/server/netty-server/sync/src/test/scala/sttp/tapir/server/netty/sync/NettySyncTestServerInterpreter.scala @@ -30,18 +30,17 @@ class NettySyncTestServerInterpreter(eventLoopGroup: NioEventLoopGroup) } } - override def serverWithStop( + override def server( routes: NonEmptyList[IdRoute], gracefulShutdownTimeout: Option[FiniteDuration] = None - ): Resource[IO, (Port, IO[Unit])] = { + ): Resource[IO, Port] = { val config = NettyConfig.default.eventLoopGroup(eventLoopGroup).randomPort.withDontShutdownEventLoopGroupOnClose.noGracefulShutdown val customizedConfig = gracefulShutdownTimeout.map(config.withGracefulShutdownTimeout).getOrElse(config) val options = NettySyncServerOptions.default val bind = IO.blocking(NettySyncServer(options, customizedConfig).start(routes.toList)) - Resource - .make(bind.map(b => (b.port, IO.blocking(b.stop())))) { case (_, stop) => stop } + Resource.make(bind)(server => IO.blocking(server.stop())).map(_.port) } def scopedServerWithRoutesStop( diff --git a/server/netty-server/zio/src/test/scala/sttp/tapir/server/netty/zio/NettyZioTestServerInterpreter.scala b/server/netty-server/zio/src/test/scala/sttp/tapir/server/netty/zio/NettyZioTestServerInterpreter.scala index ed6ad1cb7c..faea526f2b 100644 --- a/server/netty-server/zio/src/test/scala/sttp/tapir/server/netty/zio/NettyZioTestServerInterpreter.scala +++ b/server/netty-server/zio/src/test/scala/sttp/tapir/server/netty/zio/NettyZioTestServerInterpreter.scala @@ -21,10 +21,10 @@ class NettyZioTestServerInterpreter[R](eventLoopGroup: NioEventLoopGroup) NettyZioServerInterpreter(serverOptions).toRoute(es) } - override def serverWithStop( + override def server( routes: NonEmptyList[Task[Route[Task]]], gracefulShutdownTimeout: Option[FiniteDuration] = None - ): Resource[IO, (Port, KillSwitch)] = { + ): Resource[IO, Port] = { val config = NettyConfig.default .eventLoopGroup(eventLoopGroup) .randomPort @@ -46,8 +46,7 @@ class NettyZioTestServerInterpreter[R](eventLoopGroup: NioEventLoopGroup) ) Resource - .make(bind.map(b => (b.port, IO.fromFuture[Unit](IO(Unsafe.unsafe(implicit u => runtime.unsafe.runToFuture(b.stop()))))))) { - case (_, release) => release - } + .make(bind)(server => IO.fromFuture[Unit](IO(Unsafe.unsafe(implicit u => runtime.unsafe.runToFuture(server.stop()))))) + .map(_.port) } } diff --git a/server/nima-server/src/test/scala/sttp/tapir/server/nima/NimaTestServerInterpreter.scala b/server/nima-server/src/test/scala/sttp/tapir/server/nima/NimaTestServerInterpreter.scala index 5d66c68b19..4358cd507b 100644 --- a/server/nima-server/src/test/scala/sttp/tapir/server/nima/NimaTestServerInterpreter.scala +++ b/server/nima-server/src/test/scala/sttp/tapir/server/nima/NimaTestServerInterpreter.scala @@ -18,10 +18,10 @@ class NimaTestServerInterpreter() extends TestServerInterpreter[Identity, Any, N NimaServerInterpreter(serverOptions).toHandler(es) } - override def serverWithStop( + override def server( nimaRoutes: NonEmptyList[Handler], gracefulShutdownTimeout: Option[FiniteDuration] - ): Resource[IO, (Port, KillSwitch)] = { + ): Resource[IO, Port] = { val bind = IO.blocking { WebServer .builder() @@ -35,8 +35,7 @@ class NimaTestServerInterpreter() extends TestServerInterpreter[Identity, Any, N } Resource - .make(bind.map(b => (b.port, IO.blocking(b.stop()).map(_ => ())))) { case (_, release) => - release - } + .make(bind)(server => IO.blocking { val _ = server.stop() }) + .map(_.port) } } diff --git a/server/pekko-http-server/src/test/scala/sttp/tapir/server/pekkohttp/PekkoHttpTestServerInterpreter.scala b/server/pekko-http-server/src/test/scala/sttp/tapir/server/pekkohttp/PekkoHttpTestServerInterpreter.scala index 1f63934d43..0bf69fa7f0 100644 --- a/server/pekko-http-server/src/test/scala/sttp/tapir/server/pekkohttp/PekkoHttpTestServerInterpreter.scala +++ b/server/pekko-http-server/src/test/scala/sttp/tapir/server/pekkohttp/PekkoHttpTestServerInterpreter.scala @@ -23,22 +23,14 @@ class PekkoHttpTestServerInterpreter(implicit actorSystem: ActorSystem) PekkoHttpServerInterpreter(serverOptions).toRoute(es) } - override def serverWithStop( + override def server( routes: NonEmptyList[Route], gracefulShutdownTimeout: Option[FiniteDuration] - ): Resource[IO, (Port, KillSwitch)] = { + ): Resource[IO, Port] = { val bind = IO.fromFuture(IO(Http().newServerAt("localhost", 0).bind(concat(routes.toList: _*)))) Resource - .make( - bind.map(b => - ( - b.localAddress.getPort(), - IO.fromFuture(IO(b.terminate(gracefulShutdownTimeout.getOrElse(50.millis)))).void - ) - ) - ) { case (_, release) => - release - } + .make(bind)(server => IO.fromFuture(IO(server.terminate(gracefulShutdownTimeout.getOrElse(50.millis)))).void) + .map(_.localAddress.getPort) } } diff --git a/server/play-server/src/test/scala/sttp/tapir/server/play/PlayTestServerInterpreter.scala b/server/play-server/src/test/scala/sttp/tapir/server/play/PlayTestServerInterpreter.scala index db6638d025..62ca04e8f0 100644 --- a/server/play-server/src/test/scala/sttp/tapir/server/play/PlayTestServerInterpreter.scala +++ b/server/play-server/src/test/scala/sttp/tapir/server/play/PlayTestServerInterpreter.scala @@ -31,13 +31,11 @@ class PlayTestServerInterpreter(implicit actorSystem: ActorSystem) PlayServerInterpreter(serverOptions).toRoutes(es) } - import play.core.server.PekkoHttpServer - - override def serverWithStop( + override def server( routes: NonEmptyList[Routes], gracefulShutdownTimeout: Option[FiniteDuration] - ): Resource[IO, (Port, KillSwitch)] = { - val components = new DefaultPekkoHttpServerComponents { + ): Resource[IO, Port] = { + lazy val components = new DefaultPekkoHttpServerComponents { val initialServerConfig = ServerConfig(port = Some(0), address = "127.0.0.1", mode = Mode.Test) val customConf = @@ -59,9 +57,9 @@ class PlayTestServerInterpreter(implicit actorSystem: ActorSystem) }) ) } - val bind = IO { + val bind = IO.blocking { components.server } - Resource.make(bind.map(s => (s.mainAddress.getPort, IO(s.stop())))) { case (_, release) => release } + Resource.make(bind)(s => IO.blocking(s.stop())).map(s => (s.mainAddress.getPort)) } } diff --git a/server/play29-server/src/test/scala/sttp/tapir/server/play/PlayTestServerInterpreter.scala b/server/play29-server/src/test/scala/sttp/tapir/server/play/PlayTestServerInterpreter.scala index dc999c0c96..68c9eec9d4 100644 --- a/server/play29-server/src/test/scala/sttp/tapir/server/play/PlayTestServerInterpreter.scala +++ b/server/play29-server/src/test/scala/sttp/tapir/server/play/PlayTestServerInterpreter.scala @@ -31,10 +31,10 @@ class PlayTestServerInterpreter(implicit actorSystem: ActorSystem) PlayServerInterpreter(serverOptions).toRoutes(es) } - override def serverWithStop( + override def server( routes: NonEmptyList[Routes], gracefulShutdownTimeout: Option[FiniteDuration] - ): Resource[IO, (Port, KillSwitch)] = { + ): Resource[IO, Port] = { val components = new DefaultAkkaHttpServerComponents { val initialServerConfig = ServerConfig(port = Some(0), address = "127.0.0.1", mode = Mode.Test) @@ -57,9 +57,9 @@ class PlayTestServerInterpreter(implicit actorSystem: ActorSystem) }) ) } - val bind = IO { + val bind = IO.blocking { components.server } - Resource.make(bind.map(s => (s.mainAddress.getPort, IO(s.stop())))) { case (_, release) => release } + Resource.make(bind)(s => IO.blocking(s.stop())).map(s => (s.mainAddress.getPort)) } } diff --git a/server/tests/src/main/scala/sttp/tapir/server/tests/CreateServerTest.scala b/server/tests/src/main/scala/sttp/tapir/server/tests/CreateServerTest.scala index 7bb8b83823..d1909c8185 100644 --- a/server/tests/src/main/scala/sttp/tapir/server/tests/CreateServerTest.scala +++ b/server/tests/src/main/scala/sttp/tapir/server/tests/CreateServerTest.scala @@ -13,7 +13,6 @@ import sttp.tapir._ import sttp.tapir.server.ServerEndpoint import sttp.tapir.server.interceptor.CustomiseInterceptors import sttp.tapir.tests._ -import org.scalactic.anyvals.FiniteDouble import org.slf4j.LoggerFactory import scala.concurrent.duration.FiniteDuration @@ -53,7 +52,7 @@ trait CreateServerTest[F[_], +R, OPTIONS, ROUTE] { */ def testServerWithStop(name: String, rs: => NonEmptyList[ROUTE], gracefulShutdownTimeout: Option[FiniteDuration])( runTest: KillSwitch => (SttpBackend[IO, Fs2Streams[IO] with WebSockets], Uri) => IO[Assertion] - ): Test = testServer(name, rs)(runTest(IO.unit)) + ): Test } class DefaultCreateServerTest[F[_], +R, OPTIONS, ROUTE]( diff --git a/server/tests/src/main/scala/sttp/tapir/server/tests/TestServerInterpreter.scala b/server/tests/src/main/scala/sttp/tapir/server/tests/TestServerInterpreter.scala index b4c655dbc6..8bb4548295 100644 --- a/server/tests/src/main/scala/sttp/tapir/server/tests/TestServerInterpreter.scala +++ b/server/tests/src/main/scala/sttp/tapir/server/tests/TestServerInterpreter.scala @@ -18,8 +18,11 @@ trait TestServerInterpreter[F[_], +R, OPTIONS, ROUTE] { def route(es: List[ServerEndpoint[R, F]], interceptors: Interceptors = identity): ROUTE - def serverWithStop(routes: NonEmptyList[ROUTE], gracefulShutdownTimeout: Option[FiniteDuration] = None): Resource[IO, (Port, KillSwitch)] + def serverWithStop( + routes: NonEmptyList[ROUTE], + gracefulShutdownTimeout: Option[FiniteDuration] = None + ): Resource[IO, (Port, KillSwitch)] = + Resource.eval(server(routes, gracefulShutdownTimeout).allocated) - def server(routes: NonEmptyList[ROUTE]): Resource[IO, Port] = - serverWithStop(routes, gracefulShutdownTimeout = None).map(_._1) + def server(routes: NonEmptyList[ROUTE], gracefulShutdownTimeout: Option[FiniteDuration] = None): Resource[IO, Port] } diff --git a/server/vertx-server/cats/src/test/scala/sttp/tapir/server/vertx/cats/CatsVertxTestServerInterpreter.scala b/server/vertx-server/cats/src/test/scala/sttp/tapir/server/vertx/cats/CatsVertxTestServerInterpreter.scala index 9bfe0fafc3..d1f78da3a4 100644 --- a/server/vertx-server/cats/src/test/scala/sttp/tapir/server/vertx/cats/CatsVertxTestServerInterpreter.scala +++ b/server/vertx-server/cats/src/test/scala/sttp/tapir/server/vertx/cats/CatsVertxTestServerInterpreter.scala @@ -27,15 +27,15 @@ class CatsVertxTestServerInterpreter(vertx: Vertx, dispatcher: Dispatcher[IO]) es.map(interpreter.route(_)(router)).last } - override def serverWithStop( + override def server( routes: NonEmptyList[Router => Route], gracefulShutdownTimeout: Option[FiniteDuration] - ): Resource[IO, (Port, KillSwitch)] = { + ): Resource[IO, Port] = { val router = Router.router(vertx) routes.toList.foreach(_.apply(router)) val server = vertx.createHttpServer(new HttpServerOptions().setPort(0)).requestHandler(router) val listenIO = ioFromVFuture(server.listen(0)) // Vertx doesn't offer graceful shutdown with timeout OOTB - Resource.make(listenIO.map(s => (s.actualPort(), ioFromVFuture(s.close).void))) { case (_, release) => release } + Resource.make(listenIO)(s => ioFromVFuture(s.close).void).map(_.actualPort()) } } diff --git a/server/vertx-server/src/test/scala/sttp/tapir/server/vertx/VertxTestServerInterpreter.scala b/server/vertx-server/src/test/scala/sttp/tapir/server/vertx/VertxTestServerInterpreter.scala index 988eb9a212..dee9e98b57 100644 --- a/server/vertx-server/src/test/scala/sttp/tapir/server/vertx/VertxTestServerInterpreter.scala +++ b/server/vertx-server/src/test/scala/sttp/tapir/server/vertx/VertxTestServerInterpreter.scala @@ -25,16 +25,16 @@ class VertxTestServerInterpreter(vertx: Vertx) es.map(interpreter.route(_)(router)).last } - override def serverWithStop( + override def server( routes: NonEmptyList[Router => Route], gracefulShutdownTimeout: Option[FiniteDuration] - ): Resource[IO, (Port, KillSwitch)] = { + ): Resource[IO, Port] = { val router = Router.router(vertx) val server = vertx.createHttpServer(new HttpServerOptions().setPort(0)).requestHandler(router) val listenIO = vertxFutureToIo(server.listen(0)) routes.toList.foreach(_.apply(router)) // Vertx doesn't offer graceful shutdown with timeout OOTB - Resource.make(listenIO.map(s => (s.actualPort(), vertxFutureToIo(s.close()).void))) { case (_, release) => release } + Resource.make(listenIO)(s => vertxFutureToIo(s.close()).void).map(s => s.actualPort()) } } diff --git a/server/vertx-server/zio/src/test/scala/sttp/tapir/server/vertx/zio/ZioVertxTestServerInterpreter.scala b/server/vertx-server/zio/src/test/scala/sttp/tapir/server/vertx/zio/ZioVertxTestServerInterpreter.scala index 3a0e97613c..63c312d1f4 100644 --- a/server/vertx-server/zio/src/test/scala/sttp/tapir/server/vertx/zio/ZioVertxTestServerInterpreter.scala +++ b/server/vertx-server/zio/src/test/scala/sttp/tapir/server/vertx/zio/ZioVertxTestServerInterpreter.scala @@ -24,19 +24,17 @@ class ZioVertxTestServerInterpreter(vertx: Vertx) es.map(interpreter.route(_)(runtime)(router)).last } - override def serverWithStop( + override def server( routes: NonEmptyList[Router => Route], gracefulShutdownTimeout: Option[FiniteDuration] - ): Resource[IO, (Port, KillSwitch)] = { + ): Resource[IO, Port] = { val router = Router.router(vertx) val server = vertx.createHttpServer(new HttpServerOptions().setPort(0)).requestHandler(router) routes.toList.foreach(_.apply(router)) val listenIO = VertxTestServerInterpreter.vertxFutureToIo(server.listen(0)) // Vertx doesn't offer graceful shutdown with timeout OOTB - Resource.make(listenIO.map(s => (s.actualPort(), VertxTestServerInterpreter.vertxFutureToIo(s.close).void))) { case (_, release) => - release - } + Resource.make(listenIO)(server => VertxTestServerInterpreter.vertxFutureToIo(server.close).void).map(_.actualPort()) } } diff --git a/server/zio-http-server/src/test/scala/sttp/tapir/server/ziohttp/ZioHttpTestServerInterpreter.scala b/server/zio-http-server/src/test/scala/sttp/tapir/server/ziohttp/ZioHttpTestServerInterpreter.scala index 9000406891..146367809c 100644 --- a/server/zio-http-server/src/test/scala/sttp/tapir/server/ziohttp/ZioHttpTestServerInterpreter.scala +++ b/server/zio-http-server/src/test/scala/sttp/tapir/server/ziohttp/ZioHttpTestServerInterpreter.scala @@ -26,12 +26,12 @@ class ZioHttpTestServerInterpreter( ZioHttpInterpreter(serverOptions).toHttp(es) } - override def serverWithStop( + // Needs to manually call killSwitch, because serverWithStop uses `allocated` + override def server( routes: NonEmptyList[Routes[Any, Response]], - gracefulShutdownTimeout: Option[FiniteDuration] - ): Resource[IO, (Port, KillSwitch)] = { + gracefulShutdownTimeout: Option[FiniteDuration] = None + ): Resource[IO, Port] = { implicit val r: Runtime[Any] = Runtime.default - val effect: ZIO[Scope, Throwable, Port] = (for { driver <- ZIO.service[Driver] @@ -49,7 +49,7 @@ class ZioHttpTestServerInterpreter( .gracefulShutdownTimeout(gracefulShutdownTimeout.map(Duration.fromScala).getOrElse(50.millis)) ) ) - - Resource.make(Resource.scoped[IO, Any, Port](effect).allocated) { case (_, release) => release } + Resource.scoped[IO, Any, Port](effect) } + } diff --git a/serverless/aws/lambda-cats-effect-tests/src/test/scala/sttp/tapir/serverless/aws/lambda/tests/AwsLambdaCreateServerStubTest.scala b/serverless/aws/lambda-cats-effect-tests/src/test/scala/sttp/tapir/serverless/aws/lambda/tests/AwsLambdaCreateServerStubTest.scala index 318b96c05b..692da367c5 100644 --- a/serverless/aws/lambda-cats-effect-tests/src/test/scala/sttp/tapir/serverless/aws/lambda/tests/AwsLambdaCreateServerStubTest.scala +++ b/serverless/aws/lambda-cats-effect-tests/src/test/scala/sttp/tapir/serverless/aws/lambda/tests/AwsLambdaCreateServerStubTest.scala @@ -33,6 +33,10 @@ class AwsLambdaCreateServerStubTest extends CreateServerTest[IO, Any, AwsServerO Test(name)(runTest(stubBackend(route), uri"http://localhost:3001").unsafeToFuture()) } + def testServerWithStop(name: String, rs: => NonEmptyList[Route[IO]], gracefulShutdownTimeout: Option[FiniteDuration])( + runTest: KillSwitch => (SttpBackend[IO, Fs2Streams[IO] with WebSockets], Uri) => IO[Assertion] + ): Test = throw new UnsupportedOperationException + override def testServerLogic(e: ServerEndpoint[Any, IO], testNameSuffix: String, interceptors: Interceptors = identity)( runTest: (SttpBackend[IO, Fs2Streams[IO] with WebSockets], Uri) => IO[Assertion] ): Test = { diff --git a/serverless/aws/lambda-cats-effect-tests/src/test/scala/sttp/tapir/serverless/aws/lambda/tests/AwsLambdaStubHttpTest.scala b/serverless/aws/lambda-cats-effect-tests/src/test/scala/sttp/tapir/serverless/aws/lambda/tests/AwsLambdaStubHttpTest.scala index a36bf71a0c..ca2e2db83f 100644 --- a/serverless/aws/lambda-cats-effect-tests/src/test/scala/sttp/tapir/serverless/aws/lambda/tests/AwsLambdaStubHttpTest.scala +++ b/serverless/aws/lambda-cats-effect-tests/src/test/scala/sttp/tapir/serverless/aws/lambda/tests/AwsLambdaStubHttpTest.scala @@ -29,9 +29,10 @@ object AwsLambdaStubHttpTest { AwsCatsEffectServerInterpreter(serverOptions).toRoute(es) } - override def serverWithStop( + override def server( routes: NonEmptyList[Route[IO]], gracefulShutdownTimeout: Option[FiniteDuration] - ): Resource[IO, (Port, KillSwitch)] = ??? + ): Resource[IO, Port] = throw new UnsupportedOperationException + } } diff --git a/serverless/aws/lambda-zio-tests/src/test/scala/sttp/tapir/serverless/aws/ziolambda/tests/AwsLambdaCreateServerStubTest.scala b/serverless/aws/lambda-zio-tests/src/test/scala/sttp/tapir/serverless/aws/ziolambda/tests/AwsLambdaCreateServerStubTest.scala index 399987650c..70f425188c 100644 --- a/serverless/aws/lambda-zio-tests/src/test/scala/sttp/tapir/serverless/aws/ziolambda/tests/AwsLambdaCreateServerStubTest.scala +++ b/serverless/aws/lambda-zio-tests/src/test/scala/sttp/tapir/serverless/aws/ziolambda/tests/AwsLambdaCreateServerStubTest.scala @@ -40,6 +40,10 @@ class AwsLambdaCreateServerStubTest extends CreateServerTest[Task, Any, AwsServe Test(name)(runTest(stubBackend(transformMonad(route)), uri"http://localhost:3002").unsafeToFuture()) } + def testServerWithStop(name: String, rs: => NonEmptyList[Route[Task]], gracefulShutdownTimeout: Option[FiniteDuration])( + runTest: KillSwitch => (SttpBackend[IO, Fs2Streams[IO] with WebSockets], Uri) => IO[Assertion] + ): Test = throw new UnsupportedOperationException + override def testServerLogic(e: ServerEndpoint[Any, Task], testNameSuffix: String, interceptors: Interceptors = identity)( runTest: (SttpBackend[IO, Fs2Streams[IO] with WebSockets], Uri) => IO[Assertion] ): Test = { diff --git a/serverless/aws/lambda-zio-tests/src/test/scala/sttp/tapir/serverless/aws/ziolambda/tests/AwsLambdaStubHttpTest.scala b/serverless/aws/lambda-zio-tests/src/test/scala/sttp/tapir/serverless/aws/ziolambda/tests/AwsLambdaStubHttpTest.scala index df78db8f10..3cf620787b 100644 --- a/serverless/aws/lambda-zio-tests/src/test/scala/sttp/tapir/serverless/aws/ziolambda/tests/AwsLambdaStubHttpTest.scala +++ b/serverless/aws/lambda-zio-tests/src/test/scala/sttp/tapir/serverless/aws/ziolambda/tests/AwsLambdaStubHttpTest.scala @@ -35,9 +35,10 @@ object AwsLambdaStubHttpTest { AwsZioServerInterpreter(serverOptions).toRoute(es) } - override def serverWithStop( + override def server( routes: NonEmptyList[Route[Task]], gracefulShutdownTimeout: Option[FiniteDuration] - ): Resource[IO, (Port, KillSwitch)] = ??? + ): Resource[IO, Port] = throw new UnsupportedOperationException + } }