From 7f6d4218ff72fcd259eced12113fb38666c4270b Mon Sep 17 00:00:00 2001 From: Adam Warski Date: Fri, 5 Jul 2024 17:09:43 +0200 Subject: [PATCH 1/8] Use Scala 3 in the IDE, for examples & documentation (#3903) --- .github/workflows/ci.yml | 6 +- build.sbt | 158 ++++++------------ .../sttp/tapir/macros/CodecMacros.scala | 7 +- doc/client/play.md | 5 +- doc/client/sttp.md | 12 +- doc/docs/asyncapi.md | 18 +- doc/docs/json-schema.md | 70 ++++---- doc/docs/openapi.md | 40 ++--- doc/endpoint/basics.md | 6 +- doc/endpoint/contenttype.md | 6 +- doc/endpoint/customtypes.md | 23 ++- doc/endpoint/enumerations.md | 62 +++---- doc/endpoint/forms.md | 16 +- doc/endpoint/integrations.md | 34 ++-- doc/endpoint/ios.md | 24 +-- doc/endpoint/json.md | 64 ++++--- doc/endpoint/oneof.md | 34 ++-- doc/endpoint/pickler.md | 43 ++--- doc/endpoint/schemas.md | 87 +++++----- doc/endpoint/security.md | 6 +- doc/endpoint/static.md | 64 +++---- doc/endpoint/streaming.md | 10 +- doc/endpoint/validation.md | 12 +- doc/endpoint/websockets.md | 26 +-- doc/endpoint/xml.md | 26 ++- doc/examples.md | 2 +- doc/generator/sbt-openapi-codegen.md | 6 +- doc/index.md | 36 ++-- doc/mytapir.md | 8 +- doc/quickstart.md | 4 +- doc/scala_2_3_platforms.md | 53 ++++++ doc/server/akkahttp.md | 8 +- doc/server/armeria.md | 69 ++++---- doc/server/aws.md | 7 +- doc/server/errors.md | 25 ++- doc/server/finatra.md | 12 +- doc/server/http4s.md | 16 +- doc/server/jdkhttp.md | 6 +- doc/server/logic.md | 20 +-- doc/server/netty.md | 12 +- doc/server/nima.md | 2 +- doc/server/observability.md | 7 +- doc/server/options.md | 22 +-- doc/server/pekkohttp.md | 8 +- doc/server/play.md | 29 ++-- doc/server/vertx.md | 69 ++++---- doc/server/zio-http4s.md | 16 +- doc/server/ziohttp.md | 10 +- doc/testing.md | 43 +++-- .../src/main/resources/webapp/img/logo.png | Bin .../src/main/resources/webapp/index.html | 0 .../examples/HelloWorldHttp4sServer.scala | 6 +- .../examples/HelloWorldNettyCatsServer.scala | 3 +- .../examples/HelloWorldZioHttpServer.scala | 3 +- .../examples/ZioEnvExampleHttp4sServer.scala | 8 +- .../examples/ZioExampleHttp4sServer.scala | 3 +- .../examples/ZioExampleZioHttpServer.scala | 3 +- .../ZioPartialServerLogicHttp4s.scala | 15 +- ...{BooksExample.scala => booksExample.scala} | 37 ++-- ...xample.scala => booksPicklerExample.scala} | 32 ++-- .../examples/client/Http4sClientExample.scala | 13 +- .../CommaSeparatedQueryParameter.scala | 80 --------- .../EndpointWithCustomTypes.scala | 3 +- ...iauto.scala => booksExampleSemiauto.scala} | 31 ++-- .../commaSeparatedQueryParameter.scala | 73 ++++++++ .../sealedTraitWithDiscriminator.scala | 52 ++++++ .../IronRefinementErrorsNettyServer.scala | 5 +- ...tomErrorsOnDecodeFailurePekkoServer.scala} | 8 +- ...er.scala => errorOutputsPekkoServer.scala} | 6 +- ...er.scala => helloWorldArmeriaServer.scala} | 3 +- ...er.scala => helloWorldJdkHttpServer.scala} | 8 +- ...cala => helloWorldNettyFutureServer.scala} | 8 +- ....scala => helloWorldNettySyncServer.scala} | 4 +- ...rver.scala => helloWorldPekkoServer.scala} | 3 +- .../sttp/tapir/examples/logging/Logging.scala | 12 -- ...oLoggingWithCorrelationIdNettyServer.scala | 6 +- ...a => multipartFormUploadPekkoServer.scala} | 6 +- .../observability/ZioMetricsExample.scala | 7 +- ...mple.scala => datadogMetricsExample.scala} | 6 +- ...cala => openTelemetryMetricsExample.scala} | 6 +- ...e.scala => prometheusMetricsExample.scala} | 6 +- ...leEndpointsDocumentationHttp4sServer.scala | 7 +- .../RedocContextPathHttp4sServer.scala | 10 +- .../examples/openapi/RedocZioHttpServer.scala | 5 +- ...leEndpointsDocumentationPekkoServer.scala} | 6 +- ...tensions.scala => openapiExtensions.scala} | 3 +- ...scala => swaggerUIOAuth2PekkoServer.scala} | 6 +- ...Schemas.scala => customisingSchemas.scala} | 3 +- .../security/OAuth2GithubHttp4sServer.scala | 6 +- .../security/ServerSecurityLogicZio.scala | 10 +- ...a => basicAuthenticationPekkoServer.scala} | 3 +- ...scala => corsInterceptorPekkoServer.scala} | 4 +- ...cala => externalSecurityInterceptor.scala} | 3 +- ...o.scala => serverSecurityLogicPekko.scala} | 3 +- ...verSecurityLogicRefreshCookiesPekko.scala} | 3 +- .../StaticContentFromFilesNettyServer.scala | 16 -- .../staticContentFromFilesNettyServer.scala | 12 ++ ...> staticContentFromFilesPekkoServer.scala} | 3 +- ...aticContentFromResourcesPekkoServer.scala} | 5 +- ...a => staticContentSecurePekkoServer.scala} | 3 +- ...rver.scala => statusCodeNettyServer.scala} | 55 +++--- .../streaming/ProxyHttp4sFs2Server.scala | 10 +- .../streaming/StreamingHttp4sFs2Server.scala | 10 +- .../streaming/StreamingNettyFs2Server.scala | 9 +- .../streaming/StreamingNettyZioServer.scala | 8 +- .../streaming/StreamingZioHttpServer.scala | 3 +- ...erver.scala => streamingPekkoServer.scala} | 5 +- .../PekkoServerStubInterpreterExample.scala | 6 +- .../testing/SttpMockServerClientExample.scala | 5 +- .../websocket/WebSocketHttp4sServer.scala | 6 +- .../websocket/WebSocketPekkoServer.scala | 76 --------- .../websocket/webSocketPekkoServer.scala | 73 ++++++++ examples2/src/main/resources/logback.xml | 12 -- .../examples2/HelloWorldAkkaServer.scala | 38 ----- .../SealedTraitWithDiscriminator.scala | 53 ------ .../websocket/WebSocketAkkaClient.scala | 52 ------ .../tapir/json/json4s/TapirJson4sTests.scala | 2 + .../sync/NettySyncServerInterpreter.scala | 3 +- 118 files changed, 1028 insertions(+), 1314 deletions(-) create mode 100644 doc/scala_2_3_platforms.md rename {examples2 => examples}/src/main/resources/webapp/img/logo.png (100%) rename {examples2 => examples}/src/main/resources/webapp/index.html (100%) rename examples/src/main/scala/sttp/tapir/examples/{BooksExample.scala => booksExample.scala} (91%) rename examples/src/main/scala/sttp/tapir/examples/{BooksPicklerExample.scala => booksPicklerExample.scala} (92%) delete mode 100644 examples/src/main/scala/sttp/tapir/examples/custom_types/CommaSeparatedQueryParameter.scala rename examples/src/main/scala/sttp/tapir/examples/custom_types/{BooksExampleSemiauto.scala => booksExampleSemiauto.scala} (93%) create mode 100644 examples/src/main/scala/sttp/tapir/examples/custom_types/commaSeparatedQueryParameter.scala create mode 100644 examples/src/main/scala/sttp/tapir/examples/custom_types/sealedTraitWithDiscriminator.scala rename examples/src/main/scala/sttp/tapir/examples/errors/{CustomErrorsOnDecodeFailurePekkoServer.scala => customErrorsOnDecodeFailurePekkoServer.scala} (96%) rename examples/src/main/scala/sttp/tapir/examples/errors/{ErrorOutputsPekkoServer.scala => errorOutputsPekkoServer.scala} (95%) rename examples/src/main/scala/sttp/tapir/examples/{HelloWorldArmeriaServer.scala => helloWorldArmeriaServer.scala} (96%) rename examples/src/main/scala/sttp/tapir/examples/{HelloWorldJdkHttpServer.scala => helloWorldJdkHttpServer.scala} (95%) rename examples/src/main/scala/sttp/tapir/examples/{HelloWorldNettyFutureServer.scala => helloWorldNettyFutureServer.scala} (94%) rename examples/src/main/scala/sttp/tapir/examples/{HelloWorldNettySyncServer.scala => helloWorldNettySyncServer.scala} (90%) rename examples/src/main/scala/sttp/tapir/examples/{HelloWorldPekkoServer.scala => helloWorldPekkoServer.scala} (97%) delete mode 100644 examples/src/main/scala/sttp/tapir/examples/logging/Logging.scala rename examples/src/main/scala/sttp/tapir/examples/multipart/{MultipartFormUploadPekkoServer.scala => multipartFormUploadPekkoServer.scala} (96%) rename examples/src/main/scala/sttp/tapir/examples/observability/{DatadogMetricsExample.scala => datadogMetricsExample.scala} (93%) rename examples/src/main/scala/sttp/tapir/examples/observability/{OpenTelemetryMetricsExample.scala => openTelemetryMetricsExample.scala} (96%) rename examples/src/main/scala/sttp/tapir/examples/observability/{PrometheusMetricsExample.scala => prometheusMetricsExample.scala} (92%) rename examples/src/main/scala/sttp/tapir/examples/openapi/{MultipleEndpointsDocumentationPekkoServer.scala => multipleEndpointsDocumentationPekkoServer.scala} (96%) rename examples/src/main/scala/sttp/tapir/examples/openapi/{OpenapiExtensions.scala => openapiExtensions.scala} (96%) rename examples/src/main/scala/sttp/tapir/examples/openapi/{SwaggerUIOAuth2PekkoServer.scala => swaggerUIOAuth2PekkoServer.scala} (94%) rename examples/src/main/scala/sttp/tapir/examples/schema/{CustomisingSchemas.scala => customisingSchemas.scala} (98%) rename {examples2/src/main/scala/sttp/tapir/examples2 => examples/src/main/scala/sttp/tapir/examples}/security/ServerSecurityLogicZio.scala (95%) rename examples/src/main/scala/sttp/tapir/examples/security/{BasicAuthenticationPekkoServer.scala => basicAuthenticationPekkoServer.scala} (97%) rename examples/src/main/scala/sttp/tapir/examples/security/{CorsInterceptorPekkoServer.scala => corsInterceptorPekkoServer.scala} (97%) rename examples/src/main/scala/sttp/tapir/examples/security/{ExternalSecurityInterceptor.scala => externalSecurityInterceptor.scala} (98%) rename examples/src/main/scala/sttp/tapir/examples/security/{ServerSecurityLogicPekko.scala => serverSecurityLogicPekko.scala} (98%) rename examples/src/main/scala/sttp/tapir/examples/security/{ServerSecurityLogicRefreshCookiesPekko.scala => serverSecurityLogicRefreshCookiesPekko.scala} (97%) delete mode 100644 examples/src/main/scala/sttp/tapir/examples/static_content/StaticContentFromFilesNettyServer.scala create mode 100644 examples/src/main/scala/sttp/tapir/examples/static_content/staticContentFromFilesNettyServer.scala rename examples/src/main/scala/sttp/tapir/examples/static_content/{StaticContentFromFilesPekkoServer.scala => staticContentFromFilesPekkoServer.scala} (97%) rename examples/src/main/scala/sttp/tapir/examples/static_content/{StaticContentFromResourcesPekkoServer.scala => staticContentFromResourcesPekkoServer.scala} (89%) rename examples/src/main/scala/sttp/tapir/examples/static_content/{StaticContentSecurePekkoServer.scala => staticContentSecurePekkoServer.scala} (97%) rename examples/src/main/scala/sttp/tapir/examples/status_code/{StatusCodeNettyFutureServer.scala => statusCodeNettyServer.scala} (59%) rename examples/src/main/scala/sttp/tapir/examples/streaming/{StreamingPekkoServer.scala => streamingPekkoServer.scala} (93%) rename {examples2/src/main/scala/sttp/tapir/examples2 => examples/src/main/scala/sttp/tapir/examples}/testing/SttpMockServerClientExample.scala (95%) delete mode 100644 examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketPekkoServer.scala create mode 100644 examples/src/main/scala/sttp/tapir/examples/websocket/webSocketPekkoServer.scala delete mode 100644 examples2/src/main/resources/logback.xml delete mode 100644 examples2/src/main/scala/sttp/tapir/examples2/HelloWorldAkkaServer.scala delete mode 100644 examples2/src/main/scala/sttp/tapir/examples2/custom_types/SealedTraitWithDiscriminator.scala delete mode 100644 examples2/src/main/scala/sttp/tapir/examples2/websocket/WebSocketAkkaClient.scala rename json/json4s/src/test/{scalajvm => scala-2}/sttp/tapir/json/json4s/TapirJson4sTests.scala (94%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b5a07c93cd..9c35d8ca65 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,7 +74,7 @@ jobs: - name: Compile run: sbt $SBT_JAVA_OPTS -v "compileScoped ${{ matrix.scala-version }} ${{ matrix.target-platform }}" - name: Compile documentation - if: matrix.target-platform == 'JVM' && matrix.java == '11' + if: matrix.target-platform == 'JVM' && matrix.java == '21' run: sbt $SBT_JAVA_OPTS -v compileDocumentation - name: Test if: matrix.target-platform == 'JVM' && matrix.scala-version == '2.12' @@ -131,11 +131,11 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 # checkout tags so that dynver works properly (we need the version for MiMa) - - name: Set up JDK 11 + - name: Set up JDK 21 uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: 11 + java-version: 21 cache: 'sbt' - name: Check MiMa run: sbt $SBT_JAVA_OPTS -v mimaReportBinaryIssues diff --git a/build.sbt b/build.sbt index 3e7c5c7aaf..b10cd7c8ce 100644 --- a/build.sbt +++ b/build.sbt @@ -18,16 +18,13 @@ val scala2_12 = "2.12.19" val scala2_13 = "2.13.14" val scala3 = "3.3.3" -// The `idea.managed` property is set automatically by IntelliJ when it runs sbt for build or import -val ideaManaged = System.getProperty("idea.managed", "false").toBoolean -val ideScalaVersion = if (ideaManaged) scala2_13 else scala3 - val scala2Versions = List(scala2_12, scala2_13) val scala2And3Versions = scala2Versions ++ List(scala3) val scala2_13And3Versions = List(scala2_13, scala3) val codegenScalaVersions = List(scala2_12) -val examplesScalaVersions = List(scala3) -val documentationScalaVersion = scala2_13 +val examplesScalaVersion = scala3 +val documentationScalaVersion = scala3 +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") @@ -78,8 +75,12 @@ val commonSettings = commonSmlBuildSettings ++ ossPublishSettings ++ Seq( bspEnabled := !ideSkipProject.value, // slow down for CI Test / parallelExecution := false, - // remove false alarms about unused implicit definitions in macros - scalacOptions ++= Seq("-Ywarn-macros:after"), + scalacOptions ++= { + CrossVersion.partialVersion(scalaVersion.value) match { + case Some((2, _)) => Seq("-Ywarn-macros:after") // remove false alarms about unused implicit definitions in macros + case _ => Seq("-Xmax-inlines", "64") + } + }, evictionErrorLevel := Level.Info ) @@ -240,7 +241,6 @@ lazy val rawAllAggregates = core.projectRefs ++ play29Client.projectRefs ++ tests.projectRefs ++ perfTests.projectRefs ++ - examples2.projectRefs ++ examples.projectRefs ++ documentation.projectRefs ++ openapiCodegenCore.projectRefs ++ @@ -250,7 +250,7 @@ lazy val rawAllAggregates = core.projectRefs ++ derevo.projectRefs ++ awsCdk.projectRefs -lazy val loomProjects: Seq[String] = Seq(nettyServerSync, nimaServer, examples).flatMap(_.projectRefs).flatMap(projectId) +lazy val loomProjects: Seq[String] = Seq(nettyServerSync, nimaServer, examples, documentation).flatMap(_.projectRefs).flatMap(projectId) def projectId(projectRef: ProjectReference): Option[String] = projectRef match { @@ -797,7 +797,7 @@ lazy val json4s: ProjectMatrix = (projectMatrix in file("json/json4s")) scalaTest.value % Test ) ) - .jvmPlatform(scalaVersions = scala2Versions) + .jvmPlatform(scalaVersions = scala2And3Versions) .dependsOn(core) lazy val playJson: ProjectMatrix = (projectMatrix in file("json/playjson")) @@ -2023,68 +2023,6 @@ lazy val openapiCodegenCli: ProjectMatrix = (projectMatrix in file("openapi-code // other -lazy val examples2: ProjectMatrix = (projectMatrix in file("examples2")) - .settings(commonJvmSettings) - .settings( - name := "tapir-examples2", - libraryDependencies ++= Seq( - "dev.zio" %% "zio-interop-cats" % Versions.zioInteropCats, - "org.typelevel" %% "cats-effect" % Versions.catsEffect, - "org.http4s" %% "http4s-dsl" % Versions.http4s, - "org.http4s" %% "http4s-circe" % Versions.http4s, - "org.http4s" %% "http4s-blaze-server" % Versions.http4sBlazeServer, - "com.softwaremill.sttp.client3" %% "akka-http-backend" % Versions.sttp, - "com.softwaremill.sttp.client3" %% "pekko-http-backend" % Versions.sttp, - "com.softwaremill.sttp.client3" %% "async-http-client-backend-fs2" % Versions.sttp, - "com.softwaremill.sttp.client3" %% "async-http-client-backend-zio" % Versions.sttp, - "com.softwaremill.sttp.client3" %% "async-http-client-backend-cats" % Versions.sttp, - "com.softwaremill.sttp.apispec" %% "asyncapi-circe-yaml" % Versions.sttpApispec, - "com.github.jwt-scala" %% "jwt-circe" % Versions.jwtScala, - "org.mock-server" % "mockserver-netty" % Versions.mockServer, - "io.circe" %% "circe-generic-extras" % Versions.circeGenericExtras, - "io.opentelemetry" % "opentelemetry-sdk" % Versions.openTelemetry, - "io.opentelemetry" % "opentelemetry-sdk-metrics" % Versions.openTelemetry, - "io.opentelemetry" % "opentelemetry-exporter-otlp" % Versions.openTelemetry, - scalaTest.value, - logback - ), - publishArtifact := false, - Compile / run / fork := true - ) - .jvmPlatform(scalaVersions = List(scala2_13)) - .dependsOn( - akkaHttpServer, - pekkoHttpServer, - armeriaServer, - jdkhttpServer, - http4sServer, - http4sServerZio, - http4sClient, - sttpClient, - openapiDocs, - asyncapiDocs, - circeJson, - swaggerUiBundle, - redocBundle, - zioHttpServer, - nettyServer, - nettyServerCats, - nettyServerZio, - sttpStubServer, - playJson, - prometheusMetrics, - opentelemetryMetrics, - datadogMetrics, - zioMetrics, - sttpMockServer, - zioJson, - vertxServer, - vertxServerCats, - vertxServerZio, - finatraServer, - protobuf - ) - lazy val examples: ProjectMatrix = (projectMatrix in file("examples")) .settings(commonJvmSettings) .settings( @@ -2100,6 +2038,7 @@ lazy val examples: ProjectMatrix = (projectMatrix in file("examples")) "org.http4s" %% "http4s-dsl" % Versions.http4s, "org.http4s" %% "http4s-circe" % Versions.http4s, "org.http4s" %% "http4s-blaze-server" % Versions.http4sBlazeServer, + "org.mock-server" % "mockserver-netty" % Versions.mockServer, "io.opentelemetry" % "opentelemetry-sdk" % Versions.openTelemetry, "io.opentelemetry" % "opentelemetry-sdk-metrics" % Versions.openTelemetry, "io.opentelemetry" % "opentelemetry-exporter-otlp" % Versions.openTelemetry, @@ -2109,32 +2048,33 @@ lazy val examples: ProjectMatrix = (projectMatrix in file("examples")) publishArtifact := false, Compile / run / fork := true ) - .jvmPlatform(scalaVersions = examplesScalaVersions) + .jvmPlatform(scalaVersions = List(examplesScalaVersion)) .dependsOn( - datadogMetrics, - prometheusMetrics, - opentelemetryMetrics, - zioMetrics, + armeriaServer, + asyncapiDocs, circeJson, + datadogMetrics, + http4sClient, http4sServer, - pekkoHttpServer, - armeriaServer, - nettyServer, + http4sServerZio, + iron, jdkhttpServer, + nettyServer, nettyServerCats, - http4sClient, + nettyServerSync, + nettyServerZio, + opentelemetryMetrics, + pekkoHttpServer, picklerJson, + prometheusMetrics, sttpClient, + sttpMockServer, + sttpStubServer, swaggerUiBundle, - http4sServerZio, - nettyServerSync, - nettyServerZio, + redocBundle, zioHttpServer, zioJson, - redocBundle, - sttpStubServer, - asyncapiDocs, - iron + zioMetrics ) //TODO this should be invoked by compilation process, see #https://github.com/scalameta/mdoc/issues/355 @@ -2171,31 +2111,36 @@ lazy val documentation: ProjectMatrix = (projectMatrix in file("generated-doc")) ) .jvmPlatform(scalaVersions = List(documentationScalaVersion)) .dependsOn( - core % "compile->test", - testing, - akkaHttpServer, - pekkoHttpServer, armeriaServer, armeriaServerCats, armeriaServerZio, - jdkhttpServer, + asyncapiDocs, circeJson, + core % "compile->test", + datadogMetrics, enumeratum, - finatraServer, - finatraServerCats, + http4sClient, + http4sServerZio, + jdkhttpServer, jsoniterScala, - asyncapiDocs, - openapiDocs, json4s, + nettyServer, + nettyServerCats, + nettyServerSync, + openapiDocs, + opentelemetryMetrics, + pekkoHttpServer, + picklerJson, + playClient, playJson, playServer, + prometheusMetrics, sprayJson, - http4sClient, - http4sServerZio, - nettyServerCats, sttpClient, - playClient, + sttpMockServer, sttpStubServer, + swaggerUiBundle, + testing, tethysJson, uPickleJson, vertxServer, @@ -2203,13 +2148,6 @@ lazy val documentation: ProjectMatrix = (projectMatrix in file("generated-doc")) vertxServerZio, zio, zioHttpServer, - derevo, zioJson, - prometheusMetrics, - opentelemetryMetrics, - datadogMetrics, - zioMetrics, - sttpMockServer, - nettyServer, - swaggerUiBundle + zioMetrics ) diff --git a/core/src/main/scala-3/sttp/tapir/macros/CodecMacros.scala b/core/src/main/scala-3/sttp/tapir/macros/CodecMacros.scala index 5346de1105..48590f719f 100644 --- a/core/src/main/scala-3/sttp/tapir/macros/CodecMacros.scala +++ b/core/src/main/scala-3/sttp/tapir/macros/CodecMacros.scala @@ -3,12 +3,8 @@ package sttp.tapir.macros import sttp.tapir.CodecFormat.TextPlain import sttp.tapir.{Codec, SchemaAnnotations, Validator} import sttp.tapir.internal.CodecValueClassMacro -import sttp.tapir.Mapping -import sttp.tapir.DecodeResult -import sttp.tapir.DecodeResult.Value -import sttp.tapir.Schema -trait CodecMacros { +trait CodecMacros: /** Creates a codec for an enumeration, where the validator is derived using [[sttp.tapir.Validator.derivedEnumeration]]. This requires * that all subtypes of the sealed hierarchy `T` must be `object`s. @@ -64,4 +60,3 @@ trait CodecMacros { /** Creates a codec for value class based on codecs defined in `Codec` companion */ implicit inline def derivedValueClass[T <: AnyVal]: Codec[String, T, TextPlain] = CodecValueClassMacro.derivedValueClass[T] -} diff --git a/doc/client/play.md b/doc/client/play.md index adc409ea81..c33987fcb2 100644 --- a/doc/client/play.md +++ b/doc/client/play.md @@ -51,7 +51,7 @@ After providing the input parameters, the two following are returned: Example: ```scala mdoc:compile-only -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/doc/client/sttp.md b/doc/client/sttp.md index 268f2d8237..7ebdc94967 100644 --- a/doc/client/sttp.md +++ b/doc/client/sttp.md @@ -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")) diff --git a/doc/docs/asyncapi.md b/doc/docs/asyncapi.md index 395792f90e..d6bad3c745 100644 --- a/doc/docs/asyncapi.md +++ b/doc/docs/asyncapi.md @@ -15,16 +15,16 @@ object: ```scala mdoc:silent 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 mdoc:silent -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 mdoc:silent -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/doc/docs/json-schema.md b/doc/docs/json-schema.md index 56b350a9d5..9b5f2603bb 100644 --- a/doc/docs/json-schema.md +++ b/doc/docs/json-schema.md @@ -10,22 +10,22 @@ Schema generation can now be performed like in the following example: ```scala mdoc:compile-only 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 mdoc:compile-only 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/doc/docs/openapi.md b/doc/docs/openapi.md index dd7a5ae9aa..5ccbcf6189 100644 --- a/doc/docs/openapi.md +++ b/doc/docs/openapi.md @@ -22,7 +22,7 @@ with the endpoints for which the documentation is generated, will need in turn t interpreter. For example: ```scala mdoc:compile-only -import sttp.tapir._ +import sttp.tapir.* import sttp.tapir.swagger.bundle.SwaggerInterpreter import sttp.tapir.server.netty.{NettyFutureServerInterpreter, FutureRoute} @@ -77,7 +77,7 @@ object: ```scala mdoc:silent import sttp.apispec.openapi.OpenAPI -import sttp.tapir._ +import sttp.tapir.* import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter val booksListing = endpoint.in(path[String]("bookId")) @@ -115,7 +115,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 mdoc:silent -import sttp.apispec.openapi.circe.yaml._ +import sttp.apispec.openapi.circe.yaml.* println(docs.toYaml) ``` @@ -124,8 +124,8 @@ Or to JSON: ```scala mdoc:silent 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)) ``` @@ -144,8 +144,8 @@ Firstly add dependencies: and generate the documentation by importing valid extension methods and explicitly specifying the "3.0.3" version in the OpenAPI model: ```scala mdoc:compile-only 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]) @@ -178,8 +178,8 @@ or `tapir-redoc`: Then, you'll need to pass the server endpoints to your server interpreter. For example, using akka-http: ```scala mdoc:compile-only -import sttp.apispec.openapi.circe.yaml._ -import sttp.tapir._ +import sttp.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 @@ -227,7 +227,7 @@ that is they will form a single security requirement, with multiple schemes, e.g ```scala mdoc:compile-only import sttp.model.headers.WWWAuthenticateChallenge -import sttp.tapir._ +import sttp.tapir.* val multiAuthEndpoint = endpoint.post @@ -251,7 +251,7 @@ can be done in the security logic, server logic, or by mapping the inputs using ```scala mdoc:compile-only 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]) @@ -285,16 +285,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 mdoc:compile-only -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) @@ -333,7 +333,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 mdoc:compile-only -import sttp.tapir._ +import sttp.tapir.* val acceptHeader: EndpointInput[String] = header[String]("Accept").schema(_.hidden(true)) ``` diff --git a/doc/endpoint/basics.md b/doc/endpoint/basics.md index 818b37c7f9..54be500622 100644 --- a/doc/endpoint/basics.md +++ b/doc/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 mdoc:compile-only -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 mdoc:compile-only -import sttp.tapir._ +import sttp.tapir.* type PublicEndpoint[I, E, O, -R] = Endpoint[Unit, I, E, O, R] ``` @@ -40,7 +40,7 @@ case class User() ``` ```scala mdoc:compile-only -import sttp.tapir._ +import sttp.tapir.* val userEndpoint: PublicEndpoint[(UUID, Int), String, User, Any] = ??? ``` diff --git a/doc/endpoint/contenttype.md b/doc/endpoint/contenttype.md index d1c120f109..541d1dea8f 100644 --- a/doc/endpoint/contenttype.md +++ b/doc/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/doc/endpoint/customtypes.md b/doc/endpoint/customtypes.md index 6f8e6e6dc8..5e4a71ce11 100644 --- a/doc/endpoint/customtypes.md +++ b/doc/endpoint/customtypes.md @@ -46,29 +46,26 @@ need to provide two mappings: For example, to support a custom id type: ```scala mdoc:silent -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 mdoc:silent -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 mdoc:silent:nest 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/doc/endpoint/enumerations.md b/doc/endpoint/enumerations.md index 06bf3929a4..f07f8c6095 100644 --- a/doc/endpoint/enumerations.md +++ b/doc/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 mdoc:silent:reset-object -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 mdoc:silent:reset-object -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 mdoc:silent -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") ``` @@ -172,26 +168,25 @@ assumes that the low-level representation of the enumeration is a string. Encodi represent the enumeration's values in the documentation). For example, to use an enum as part of a `jsonBody`, using the circe library for JSON parsing/serialisation, and automatic schema derivation for case classes: -```scala mdoc:silent:reset-object -import io.circe._ -import io.circe.generic.auto._ -import sttp.tapir._ -import sttp.tapir.json.circe._ -import sttp.tapir.generic.auto._ +```scala +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 mdoc:silent:reset-object -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 mdoc:silent:reset-object -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 diff --git a/doc/endpoint/forms.md b/doc/endpoint/forms.md index 5d02d1c854..e7b855c995 100644 --- a/doc/endpoint/forms.md +++ b/doc/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 mdoc:compile-only -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 mdoc:compile-only -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 mdoc:compile-only -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 mdoc:compile-only -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 mdoc:compile-only -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/doc/endpoint/integrations.md b/doc/endpoint/integrations.md index 504d12a692..df96382423 100644 --- a/doc/endpoint/integrations.md +++ b/doc/endpoint/integrations.md @@ -15,8 +15,8 @@ datatypes as well as additional syntax: "com.softwaremill.sttp.tapir" %% "tapir-cats" % "@VERSION@" ``` -- `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: @@ -35,7 +35,7 @@ validators for `T Refined P` as long as a codec for `T` already exists: ``` 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. @@ -58,7 +58,7 @@ validators for `T :| P` as long as a codec for `T` already exists: 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: @@ -147,7 +148,7 @@ enumerations. To use, add the following dependency: "com.softwaremill.sttp.tapir" %% "tapir-enumeratum" % "@VERSION@" ``` -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`. @@ -160,7 +161,7 @@ schemas for types with a `@newtype` and `@newsubtype` annotations as long as a c "com.softwaremill.sttp.tapir" %% "tapir-newtype" % "@VERSION@" ``` -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 @@ -171,7 +172,7 @@ schemas for types which extend `NewtypeWrapped` and `NewsubtypeWrapped` annotati "com.softwaremill.sttp.tapir" %% "tapir-monix-newtype" % "@VERSION@" ``` -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 @@ -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]] ``` @@ -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/doc/endpoint/ios.md b/doc/endpoint/ios.md index ad58a2390d..93c9d70dd8 100644 --- a/doc/endpoint/ios.md +++ b/doc/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 mdoc:compile-only -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 mdoc:silent -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 mdoc:silent:reset -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 mdoc:silent:reset -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 mdoc:silent:nest -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 mdoc:silent:reset -import sttp.tapir.EndpointIO.annotations._ +import sttp.tapir.EndpointIO.annotations.* @endpointInput("books/{year}/{genre}") case class Book( diff --git a/doc/endpoint/json.md b/doc/endpoint/json.md index b9e3c4d6bc..81116b6638 100644 --- a/doc/endpoint/json.md +++ b/doc/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 mdoc:compile-only -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]) ``` @@ -56,7 +56,7 @@ To use [Circe](https://github.com/circe/circe), add the following dependency to Next, import the package (or extend the `TapirJsonCirce` trait, see [MyTapir](../mytapir.md)): ```scala mdoc:compile-only -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 mdoc:compile-only -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 mdoc:compile-only -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 mdoc:compile-only -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 @@ -129,22 +128,21 @@ To use [µPickle](http://www.lihaoyi.com/upickle/) add the following dependency Next, import the package (or extend the `TapirJsonuPickle` trait, see [MyTapir](../mytapir.md) and add `TapirJsonuPickle` not `TapirCirceJson`): ```scala mdoc:compile-only -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 mdoc:compile-only -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] ``` @@ -170,7 +168,7 @@ For **Play 2.9** use: Next, import the package (or extend the `TapirJsonPlay` trait, see [MyTapir](../mytapir.md) and add `TapirJsonPlay` not `TapirCirceJson`): ```scala mdoc:compile-only -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. @@ -186,7 +184,7 @@ To use [Spray JSON](https://github.com/spray/spray-json) add the following depen Next, import the package (or extend the `TapirJsonSpray` trait, see [MyTapir](../mytapir.md) and add `TapirJsonSpray` not `TapirCirceJson`): ```scala mdoc:compile-only -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. @@ -202,7 +200,7 @@ To use [Tethys JSON](https://github.com/tethys-json/tethys) add the following de Next, import the package (or extend the `TapirJsonTethys` trait, see [MyTapir](../mytapir.md) and add `TapirJsonTethys` not `TapirCirceJson`): ```scala mdoc:compile-only -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. @@ -218,7 +216,7 @@ To use [Jsoniter-scala](https://github.com/plokhotnyuk/jsoniter-scala) add the f Next, import the package (or extend the `TapirJsonJsoniter` trait, see [MyTapir](../mytapir.md) and add `TapirJsonJsoniter` not `TapirCirceJson`): ```scala mdoc:compile-only -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. @@ -242,16 +240,16 @@ And one of the implementations: Next, import the package (or extend the `TapirJson4s` trait, see [MyTapir](../mytapir.md) and add `TapirJson4s` instead of `TapirCirceJson`): ```scala mdoc:compile-only -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 @@ -264,7 +262,7 @@ To use [zio-json](https://github.com/zio/zio-json), add the following dependency Next, import the package (or extend the `TapirJsonZio` trait, see [MyTapir](../mytapir.md) and add `TapirJsonZio` instead of `TapirCirceJson`): ```scala mdoc:compile-only -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 mdoc:compile-only -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/doc/endpoint/oneof.md b/doc/endpoint/oneof.md index be328556a6..97c86970e1 100644 --- a/doc/endpoint/oneof.md +++ b/doc/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 mdoc:fail -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) @@ -94,11 +94,11 @@ val baseEndpoint = endpoint.errorOut( The solution is therefore to handwrite a function checking that a value (of type `Any`) is of the correct type: ```scala mdoc:invisible -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) @@ -161,7 +161,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 mdoc:compile-only -import sttp.tapir._ +import sttp.tapir.* trait DomainException { def help: String @@ -194,10 +194,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 mdoc:compile-only -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/doc/endpoint/pickler.md b/doc/endpoint/pickler.md index 8850ac7ad3..b7d17fc990 100644 --- a/doc/endpoint/pickler.md +++ b/doc/endpoint/pickler.md @@ -18,8 +18,9 @@ Please note that it is available only for Scala 3 and Scala.JS 3. A pickler can be derived directly using `Pickler.derived[T]`. This will derive both schema and `JsonCodec[T]`: -```scala +```scala mdoc:compile-only 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 mdoc:compile-only 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 mdoc:compile-only 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 mdoc:compile-only +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 mdoc:compile-only +import sttp.tapir.json.pickler.PicklerConfiguration // encodes a case object as { "$type": "MyType" } given PicklerConfiguration = PicklerConfiguration.default @@ -106,8 +107,8 @@ 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 mdoc:compile-only +import sttp.tapir.json.pickler.PicklerConfiguration // encodes a case object as { "who_am_i": "full.pkg.path.MyType" } given customConfiguration: PicklerConfiguration = @@ -121,16 +122,16 @@ The discriminator will be added as a field to all coproduct child codecs and sch 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 mdoc:compile-only +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: @@ -185,7 +186,7 @@ pResponse.schema If sealed hierarchy or enum contain case classes with parameters, they are no longer an "enumeration", and will be treated as standard sealed hierarchies (coproducts): -```scala +```scala mdoc:compile-only import sttp.tapir.json.pickler.* sealed trait 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/doc/endpoint/schemas.md b/doc/endpoint/schemas.md index f31804d563..b2d1a68872 100644 --- a/doc/endpoint/schemas.md +++ b/doc/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 mdoc:silent:reset 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 mdoc:silent 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 @@ -131,8 +130,7 @@ representation is described in documentation: ```scala mdoc:silent 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 mdoc:silent -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 mdoc:silent:reset 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 mdoc:silent:reset -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 mdoc:silent:reset -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 mdoc:silent:reset -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 mdoc:silent:reset-object -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/doc/endpoint/security.md b/doc/endpoint/security.md index 04679d4246..0fd010c1ae 100644 --- a/doc/endpoint/security.md +++ b/doc/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 @@ -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 mdoc:compile-only -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/doc/endpoint/static.md b/doc/endpoint/static.md index 98736f7fee..67ec818ee1 100644 --- a/doc/endpoint/static.md +++ b/doc/endpoint/static.md @@ -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 mdoc:compile-only -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 mdoc:compile-only -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 mdoc:compile-only 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 mdoc:compile-only -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/doc/endpoint/streaming.md b/doc/endpoint/streaming.md index c03d6c3bbc..f0c0777366 100644 --- a/doc/endpoint/streaming.md +++ b/doc/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 mdoc:silent:reset -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/doc/endpoint/validation.md b/doc/endpoint/validation.md index 0dd49bd42a..cf36251e88 100644 --- a/doc/endpoint/validation.md +++ b/doc/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 mdoc:compile-only -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 mdoc:compile-only -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 mdoc:compile-only -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 mdoc:silent:reset-object -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/doc/endpoint/websockets.md b/doc/endpoint/websockets.md index 56712d20be..3a6e4a7884 100644 --- a/doc/endpoint/websockets.md +++ b/doc/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 mdoc:silent -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 mdoc:silent -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/doc/endpoint/xml.md b/doc/endpoint/xml.md index 4ad792cda0..3720817958 100644 --- a/doc/endpoint/xml.md +++ b/doc/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/doc/examples.md b/doc/examples.md index 04090a8a26..704d70b0e2 100644 --- a/doc/examples.md +++ b/doc/examples.md @@ -1,6 +1,6 @@ # Examples -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 [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. ## Generate a tapir project diff --git a/doc/generator/sbt-openapi-codegen.md b/doc/generator/sbt-openapi-codegen.md index 69b28c6ea6..96365c94aa 100644 --- a/doc/generator/sbt-openapi-codegen.md +++ b/doc/generator/sbt-openapi-codegen.md @@ -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") ``` diff --git a/doc/index.md b/doc/index.md index a2d128bce7..a46fe5fe86 100644 --- a/doc/index.md +++ b/doc/index.md @@ -1,7 +1,7 @@ # tapir
-

Declarative, type-safe web endpoints library.

+

Rapid development of self-documenting APIs

tapir
@@ -38,6 +38,8 @@ input and output parameters. An endpoint specification can be interpreted as: 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 licensed under Apache2, the source code is [available on GitHub](https://github.com/softwaremill/tapir). + ## Why tapir? * **type-safety**: compile-time guarantees, develop-time completions, read-time information @@ -61,17 +63,6 @@ for a more detailed description of how tapir works! ScalaDocs are available at [ > ``` -## Availability - -Tapir is available: - -* 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 - -Tapir is licensed under Apache2, the source code is [available on GitHub](https://github.com/softwaremill/tapir). - ## 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. @@ -119,10 +110,10 @@ Thank you! ## Code teaser ```scala mdoc:compile-only -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 +135,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 +154,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() @@ -208,6 +199,7 @@ We offer commercial support for sttp and related technologies, as well as develo quickstart examples stability + scala_2_3_platforms .. toctree:: :maxdepth: 2 diff --git a/doc/mytapir.md b/doc/mytapir.md index b7c075abf0..7560a8fda4 100644 --- a/doc/mytapir.md +++ b/doc/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 mdoc:compile-only -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/doc/quickstart.md b/doc/quickstart.md index a7d80fc392..3fd1414079 100644 --- a/doc/quickstart.md +++ b/doc/quickstart.md @@ -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: @@ -32,4 +32,4 @@ provide type arguments in some cases). In sbt, this is: ```scala scalacOptions += "-Ypartial-unification" -``` \ No newline at end of file +``` diff --git a/doc/scala_2_3_platforms.md b/doc/scala_2_3_platforms.md new file mode 100644 index 0000000000..5534455db6 --- /dev/null +++ b/doc/scala_2_3_platforms.md @@ -0,0 +1,53 @@ +# Scala 2, Scala 3 & platforms + +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. + +## 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 + } +} +``` diff --git a/doc/server/akkahttp.md b/doc/server/akkahttp.md index 9f892737e3..1531181d64 100644 --- a/doc/server/akkahttp.md +++ b/doc/server/akkahttp.md @@ -16,7 +16,7 @@ your own Akka version (for example 2.5), use sbt exclusion. Mind the Scala versi Now import the object: -```scala mdoc:compile-only +```scala import sttp.tapir.server.akkahttp.AkkaHttpServerInterpreter ``` @@ -27,7 +27,7 @@ The `toRoute` method requires a single, or a list of `ServerEndpoint`s, which ca For example: -```scala mdoc:compile-only +```scala import sttp.tapir._ import sttp.tapir.server.akkahttp.AkkaHttpServerInterpreter import scala.concurrent.Future @@ -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 mdoc:compile-only +```scala import sttp.tapir._ import sttp.tapir.server.akkahttp.AkkaHttpServerInterpreter import akka.http.scaladsl.server._ @@ -101,7 +101,7 @@ The interpreter supports [SSE (Server Sent Events)](https://developer.mozilla.or For example, to define an endpoint that returns event stream: -```scala mdoc:compile-only +```scala import akka.stream.scaladsl.Source import sttp.model.sse.ServerSentEvent import sttp.tapir._ diff --git a/doc/server/armeria.md b/doc/server/armeria.md index 21994dcdcd..9afac992f3 100644 --- a/doc/server/armeria.md +++ b/doc/server/armeria.md @@ -22,32 +22,28 @@ The `toService` method require a single, or a list of `ServerEndpoint`s, which c [server logic](logic.md) to an endpoint. ```scala mdoc:compile-only -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 mdoc:compile-only - 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 @@ -60,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)) ``` @@ -89,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 mdoc:compile-only -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]] = ??? @@ -117,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 mdoc:compile-only 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 @@ -141,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] = ??? @@ -170,14 +163,15 @@ An HTTP server can then be started as in the following example: ```scala mdoc:compile-only 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] = ??? @@ -191,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/doc/server/aws.md b/doc/server/aws.md index 40ce4536e7..aaeea4a751 100644 --- a/doc/server/aws.md +++ b/doc/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 diff --git a/doc/server/errors.md b/doc/server/errors.md index eb8a2503e4..3e9b5dcc20 100644 --- a/doc/server/errors.md +++ b/doc/server/errors.md @@ -21,12 +21,12 @@ If the business logic signals errors as exceptions, some or all can be recovered For example: ```scala mdoc:compile-only -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 mdoc:compile-only -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 mdoc:compile-only -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/doc/server/finatra.md b/doc/server/finatra.md index 9876d4a79e..eb3f426ac6 100644 --- a/doc/server/finatra.md +++ b/doc/server/finatra.md @@ -9,7 +9,7 @@ dependency: and import the object: -```scala mdoc:compile-only +```scala import sttp.tapir.server.finatra.FinatraServerInterpreter ``` @@ -22,7 +22,7 @@ Or, if you would like to use cats-effect project, you can add the following depe and import the object: -```scala mdoc:compile-only +```scala import sttp.tapir.server.finatra.cats.FinatraCatsServerInterpreter ``` @@ -33,7 +33,7 @@ The `toRoute` method on the interpreter requires a `ServerEndpoint`, which can b For example: -```scala mdoc:compile-only +```scala import sttp.tapir._ import sttp.tapir.server.finatra.{ FinatraServerInterpreter, FinatraRoute } import com.twitter.util.Future @@ -50,7 +50,7 @@ val countCharactersRoute: FinatraRoute = or a cats-effect's example: -```scala mdoc:compile-only +```scala import cats.effect.IO import cats.effect.std.Dispatcher import sttp.tapir._ @@ -72,7 +72,7 @@ val countCharactersRoute: FinatraRoute = Now that you've created the `FinatraRoute`, add `TapirController` as a trait to your `Controller`. You can then add the created route with `addTapirRoute`. -```scala mdoc:compile-only +```scala import sttp.tapir.server.finatra._ import com.twitter.finatra.http.Controller @@ -80,4 +80,4 @@ val aRoute: FinatraRoute = ??? class MyController extends Controller with TapirController { addTapirRoute(aRoute) } -``` \ No newline at end of file +``` diff --git a/doc/server/http4s.md b/doc/server/http4s.md index 693f4ba28d..84f5ed92dc 100644 --- a/doc/server/http4s.md +++ b/doc/server/http4s.md @@ -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 mdoc:compile-only -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 mdoc:compile-only 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 mdoc:compile-only 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 mdoc:compile-only -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/doc/server/jdkhttp.md b/doc/server/jdkhttp.md index 4e12f70be7..bdcef50420 100644 --- a/doc/server/jdkhttp.md +++ b/doc/server/jdkhttp.md @@ -11,7 +11,7 @@ To expose endpoints using the 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 mdoc:compile-only -import sttp.tapir._ -import sttp.tapir.server.jdkhttp._ +import sttp.tapir.* +import sttp.tapir.server.jdkhttp.* val helloWorld = endpoint .get diff --git a/doc/server/logic.md b/doc/server/logic.md index c76b1d8136..1018756ac9 100644 --- a/doc/server/logic.md +++ b/doc/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 mdoc:compile-only -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 mdoc:compile-only -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 mdoc:compile-only -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 mdoc:silent -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 mdoc:compile-only -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/doc/server/netty.md b/doc/server/netty.md index f16dd47a1b..4cbeafa9d0 100644 --- a/doc/server/netty.md +++ b/doc/server/netty.md @@ -29,7 +29,7 @@ to an endpoint. For example: ```scala mdoc:compile-only -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 mdoc:compile-only 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 mdoc:compile-only 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 mdoc:compile-only +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/doc/server/nima.md b/doc/server/nima.md index 23661ed8e8..1f684170e9 100644 --- a/doc/server/nima.md +++ b/doc/server/nima.md @@ -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/doc/server/observability.md b/doc/server/observability.md index b14c0ab38f..8722a97f94 100644 --- a/doc/server/observability.md +++ b/doc/server/observability.md @@ -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/doc/server/options.md b/doc/server/options.md index 1e97d4e4fa..ccab80a4aa 100644 --- a/doc/server/options.md +++ b/doc/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 mdoc:compile-only 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 mdoc:compile-only 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/doc/server/pekkohttp.md b/doc/server/pekkohttp.md index 0c66500297..d9f3d533a2 100644 --- a/doc/server/pekkohttp.md +++ b/doc/server/pekkohttp.md @@ -28,7 +28,7 @@ The `toRoute` method requires a single, or a list of `ServerEndpoint`s, which ca For example: ```scala mdoc:compile-only -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 mdoc:compile-only -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 mdoc:compile-only 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/doc/server/play.md b/doc/server/play.md index cb39680308..295a346d48 100644 --- a/doc/server/play.md +++ b/doc/server/play.md @@ -58,13 +58,13 @@ The `toRoutes` method requires a single, or a list of `ServerEndpoint`s, which c ```scala mdoc:compile-only 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/doc/server/vertx.md b/doc/server/vertx.md index 7bd65d41bc..4d39ee5883 100644 --- a/doc/server/vertx.md +++ b/doc/server/vertx.md @@ -14,7 +14,7 @@ to use this interpreter with `Future`. Then import the object: ```scala mdoc:compile-only -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 mdoc:compile-only -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 @@ -69,7 +66,7 @@ to use this interpreter with Cats Effect typeclasses. Then import the object: ```scala mdoc:compile-only -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 mdoc:compile-only -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 mdoc:compile-only -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 = @@ -152,7 +147,7 @@ to use this interpreter with ZIO. Then import the object: ```scala mdoc:compile-only -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/doc/server/zio-http4s.md b/doc/server/zio-http4s.md index c4b6d4769a..ceca6dc951 100644 --- a/doc/server/zio-http4s.md +++ b/doc/server/zio-http4s.md @@ -17,17 +17,17 @@ 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](../mytapir.md)): ```scala mdoc:compile-only -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 mdoc:compile-only 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/doc/server/ziohttp.md b/doc/server/ziohttp.md index 256e82b345..6fce08f20e 100644 --- a/doc/server/ziohttp.md +++ b/doc/server/ziohttp.md @@ -17,17 +17,17 @@ 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](../mytapir.md)): ```scala mdoc:compile-only -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 mdoc:compile-only 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/doc/testing.md b/doc/testing.md index 8ac8d8996a..01502e01cb 100644 --- a/doc/testing.md +++ b/doc/testing.md @@ -26,10 +26,10 @@ dependency: "com.softwaremill.sttp.tapir" %% "tapir-sttp-stub-server" % "@VERSION@" ``` -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 mdoc:silent -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 mdoc:silent 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 mdoc:silent 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 mdoc:silent -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 @@ -149,16 +146,16 @@ And the following imports: ```scala mdoc:silent import sttp.client3.testing.SttpBackendStub -import sttp.tapir.server.stub._ +import sttp.tapir.server.stub.* ``` Then, given the following endpoint: ```scala mdoc:silent -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) @@ -203,16 +200,16 @@ Add the following dependency: Imports: ```scala mdoc:silent -import sttp.tapir.server.mockserver._ +import sttp.tapir.server.mockserver.* ``` Then, given the following endpoint: ```scala mdoc:silent -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) @@ -356,4 +353,4 @@ Results in: ```scala mdoc result2.toString -``` \ No newline at end of file +``` diff --git a/examples2/src/main/resources/webapp/img/logo.png b/examples/src/main/resources/webapp/img/logo.png similarity index 100% rename from examples2/src/main/resources/webapp/img/logo.png rename to examples/src/main/resources/webapp/img/logo.png diff --git a/examples2/src/main/resources/webapp/index.html b/examples/src/main/resources/webapp/index.html similarity index 100% rename from examples2/src/main/resources/webapp/index.html rename to examples/src/main/resources/webapp/index.html diff --git a/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala index 1dac16a447..a6b444ccf7 100644 --- a/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala @@ -12,7 +12,7 @@ import sttp.tapir.server.http4s.Http4sServerInterpreter import scala.concurrent.ExecutionContext -object HelloWorldHttp4sServer extends IOApp { +object HelloWorldHttp4sServer extends IOApp: // the endpoint: single fixed path input ("hello"), single query parameter // corresponds to: GET /hello?name=... val helloWorld: PublicEndpoint[String, Unit, String, Any] = @@ -24,7 +24,7 @@ object HelloWorldHttp4sServer extends IOApp { implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global - override def run(args: List[String]): IO[ExitCode] = { + override def run(args: List[String]): IO[ExitCode] = // starting the server BlazeServerBuilder[IO] .withExecutionContext(ec) @@ -40,5 +40,3 @@ object HelloWorldHttp4sServer extends IOApp { } } .as(ExitCode.Success) - } -} diff --git a/examples/src/main/scala/sttp/tapir/examples/HelloWorldNettyCatsServer.scala b/examples/src/main/scala/sttp/tapir/examples/HelloWorldNettyCatsServer.scala index eaf606a3b4..0ba70e423c 100644 --- a/examples/src/main/scala/sttp/tapir/examples/HelloWorldNettyCatsServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/HelloWorldNettyCatsServer.scala @@ -7,7 +7,7 @@ import sttp.shared.Identity import sttp.tapir.server.netty.cats.NettyCatsServer import sttp.tapir.* -object HelloWorldNettyCatsServer extends IOApp.Simple { +object HelloWorldNettyCatsServer extends IOApp.Simple: // One endpoint on GET /hello with query parameter `name` val helloWorldEndpoint: PublicEndpoint[String, Unit, String, Any] = endpoint.get.in("hello").in(query[String]("name")).out(stringBody) @@ -53,4 +53,3 @@ object HelloWorldNettyCatsServer extends IOApp.Simple { .guarantee(binding.stop()) } yield result } -} diff --git a/examples/src/main/scala/sttp/tapir/examples/HelloWorldZioHttpServer.scala b/examples/src/main/scala/sttp/tapir/examples/HelloWorldZioHttpServer.scala index a70a84710f..53c6bbf1ca 100644 --- a/examples/src/main/scala/sttp/tapir/examples/HelloWorldZioHttpServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/HelloWorldZioHttpServer.scala @@ -9,7 +9,7 @@ import zio.* import zio.http.{Response => ZioHttpResponse, Routes, Server} import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} -object HelloWorldZioHttpServer extends ZIOAppDefault { +object HelloWorldZioHttpServer extends ZIOAppDefault: // a simple string-only endpoint val helloWorld: PublicEndpoint[String, Unit, String, Any] = endpoint.get @@ -44,4 +44,3 @@ object HelloWorldZioHttpServer extends ZIOAppDefault { Server.live ) .exitCode -} diff --git a/examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala index 74df29d364..35af2d0e8f 100644 --- a/examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/ZioEnvExampleHttp4sServer.scala @@ -12,9 +12,9 @@ import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter import sttp.tapir.swagger.bundle.SwaggerInterpreter import sttp.tapir.ztapir.* import zio.interop.catz.* -import zio.{Console, IO, Layer, RIO, ZIO, ZIOAppDefault, ZLayer} +import zio.{Console, ExitCode, IO, Layer, RIO, URIO, ZIO, ZIOAppDefault, ZLayer} -object ZioEnvExampleHttp4sServer extends ZIOAppDefault { +object ZioEnvExampleHttp4sServer extends ZIOAppDefault: // Domain classes, services, layers case class Pet(species: String, url: String) @@ -73,6 +73,4 @@ object ZioEnvExampleHttp4sServer extends ZIOAppDefault { } - override def run = - serve.provide(PetService.live).exitCode -} + override def run: URIO[Any, ExitCode] = serve.provide(PetService.live).exitCode diff --git a/examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala b/examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala index 05e157ae58..4ae638389f 100644 --- a/examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/ZioExampleHttp4sServer.scala @@ -14,7 +14,7 @@ import sttp.tapir.ztapir.* import zio.interop.catz.* import zio.{ExitCode, Task, URIO, ZIO, ZIOAppDefault} -object ZioExampleHttp4sServer extends ZIOAppDefault { +object ZioExampleHttp4sServer extends ZIOAppDefault: case class Pet(species: String, url: String) // Sample endpoint, with the logic implemented directly using .toRoutes @@ -61,4 +61,3 @@ object ZioExampleHttp4sServer extends ZIOAppDefault { ) override def run: URIO[Any, ExitCode] = serve.exitCode -} diff --git a/examples/src/main/scala/sttp/tapir/examples/ZioExampleZioHttpServer.scala b/examples/src/main/scala/sttp/tapir/examples/ZioExampleZioHttpServer.scala index 3bb2ba9d82..f3d49a2264 100644 --- a/examples/src/main/scala/sttp/tapir/examples/ZioExampleZioHttpServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/ZioExampleZioHttpServer.scala @@ -10,7 +10,7 @@ import sttp.tapir.ztapir.* import zio.http.{Response => ZioHttpResponse, Routes, Server} import zio.{ExitCode, Task, URIO, ZIO, ZIOAppDefault, ZLayer} -object ZioExampleZioHttpServer extends ZIOAppDefault { +object ZioExampleZioHttpServer extends ZIOAppDefault: case class Pet(species: String, url: String) // Sample endpoint, with the logic implemented directly using .toRoutes @@ -48,4 +48,3 @@ object ZioExampleZioHttpServer extends ZIOAppDefault { Server.live ) .exitCode -} diff --git a/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala b/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala index 1260aeb15e..02618e483b 100644 --- a/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala +++ b/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala @@ -12,7 +12,7 @@ import zio.Console.printLine import zio.* import zio.interop.catz.* -object ZioPartialServerLogicHttp4s extends ZIOAppDefault { +object ZioPartialServerLogicHttp4s extends ZIOAppDefault: def greet(user: User, salutation: String): ZIO[Any, Nothing, String] = { val greeting = s"$salutation, ${user.name}!" @@ -79,18 +79,17 @@ object ZioPartialServerLogicHttp4s extends ZIOAppDefault { .provide(UserService.live) .exitCode ) -} +end ZioPartialServerLogicHttp4s -object UserAuthenticationLayer { +object UserAuthenticationLayer: type UserService = UserService.Service case class User(name: String) val AuthenticationErrorCode = 1001 - object UserService { - trait Service { + object UserService: + trait Service: def auth(token: String): IO[Int, User] - } val live: ZLayer[Any, Nothing, Service] = ZLayer.succeed(new Service { def auth(token: String): IO[Int, User] = { @@ -100,6 +99,4 @@ object UserAuthenticationLayer { }) def auth(token: String): ZIO[UserService, Int, User] = ZIO.environmentWithZIO(_.get.auth(token)) - } - -} + end UserService diff --git a/examples/src/main/scala/sttp/tapir/examples/BooksExample.scala b/examples/src/main/scala/sttp/tapir/examples/booksExample.scala similarity index 91% rename from examples/src/main/scala/sttp/tapir/examples/BooksExample.scala rename to examples/src/main/scala/sttp/tapir/examples/booksExample.scala index 9a588d0683..77ff388654 100644 --- a/examples/src/main/scala/sttp/tapir/examples/BooksExample.scala +++ b/examples/src/main/scala/sttp/tapir/examples/booksExample.scala @@ -1,9 +1,11 @@ package sttp.tapir.examples -import sttp.tapir.examples.logging.Logging import sttp.tapir.generic.auto.* -object BooksExample extends App with Logging { +@main def booksExample(): Unit = + import org.slf4j.{Logger, LoggerFactory} + val logger: Logger = LoggerFactory.getLogger(getClass().getName) + type Limit = Option[Int] type AuthToken = String @@ -13,9 +15,8 @@ object BooksExample extends App with Logging { case class Book(title: String, genre: Genre, year: Int, author: Author) case class BooksQuery(genre: Option[String], limit: Limit) - /** Descriptions of endpoints used in the example. - */ - object Endpoints { + /** Descriptions of endpoints used in the example. */ + object Endpoints: import io.circe.generic.auto.* import sttp.tapir.* import sttp.tapir.json.circe.* @@ -44,11 +45,11 @@ object BooksExample extends App with Logging { val booksListingByGenre: PublicEndpoint[BooksQuery, String, Vector[Book], Any] = baseEndpoint.get .in(("list" / path[String]("genre").map(Option(_))(_.get)).and(limitParameter).mapTo[BooksQuery]) .out(jsonBody[Vector[Book]]) - } + end Endpoints // - object Library { + object Library: import java.util.concurrent.atomic.AtomicReference val Books = new AtomicReference( @@ -67,7 +68,7 @@ object BooksExample extends App with Logging { ) ) - def getBooks(query: BooksQuery): Vector[Book] = { + def getBooks(query: BooksQuery): Vector[Book] = val allBooks = Books.get() val limitedBooks = query.limit match { case None => allBooks @@ -78,8 +79,7 @@ object BooksExample extends App with Logging { case Some(g) => limitedBooks.filter(_.genre.name.equalsIgnoreCase(g)) } filteredBooks - } - } + end Library // @@ -87,7 +87,7 @@ object BooksExample extends App with Logging { import sttp.tapir.server.ServerEndpoint import scala.concurrent.Future - def booksServerEndpoints: List[ServerEndpoint[Any, Future]] = { + def booksServerEndpoints: List[ServerEndpoint[Any, Future]] = import scala.concurrent.ExecutionContext.Implicits.global def bookAddLogic(book: Book, token: AuthToken): Future[Either[String, Unit]] = @@ -119,17 +119,17 @@ object BooksExample extends App with Logging { booksListing.serverLogic(bookListingLogic), booksListingByGenre.serverLogic(bookListingByGenreLogic) ) - } + end booksServerEndpoints - def swaggerUIServerEndpoints: List[ServerEndpoint[Any, Future]] = { + def swaggerUIServerEndpoints: List[ServerEndpoint[Any, Future]] = import sttp.tapir.swagger.bundle.SwaggerInterpreter // interpreting the endpoint descriptions as yaml openapi documentation // exposing the docs using SwaggerUI endpoints, interpreted as an pekko-http route SwaggerInterpreter().fromEndpoints(List(addBook, booksListing, booksListingByGenre), "The Tapir Library", "1.0") - } + end swaggerUIServerEndpoints - def startServer(serverEndpoints: List[ServerEndpoint[Any, Future]]): Unit = { + def startServer(serverEndpoints: List[ServerEndpoint[Any, Future]]): Unit = import org.apache.pekko.actor.ActorSystem import org.apache.pekko.http.scaladsl.Http @@ -144,9 +144,9 @@ object BooksExample extends App with Logging { Await.result(Http().newServerAt("localhost", 8080).bindFlow(routes), 1.minute) logger.info("Server started") - } + end startServer - def makeClientRequest(): Unit = { + def makeClientRequest(): Unit = import sttp.client3.* import sttp.tapir.client.sttp.SttpClientInterpreter @@ -155,7 +155,7 @@ object BooksExample extends App with Logging { val result: Either[String, Vector[Book]] = client(Some(3)) logger.info("Result of listing request with limit 3: " + result) - } + end makeClientRequest logger.info("Welcome to the Tapir Library example!") @@ -166,4 +166,3 @@ object BooksExample extends App with Logging { makeClientRequest() logger.info("Try out the API by opening the Swagger UI: http://localhost:8080/docs") -} diff --git a/examples/src/main/scala/sttp/tapir/examples/BooksPicklerExample.scala b/examples/src/main/scala/sttp/tapir/examples/booksPicklerExample.scala similarity index 92% rename from examples/src/main/scala/sttp/tapir/examples/BooksPicklerExample.scala rename to examples/src/main/scala/sttp/tapir/examples/booksPicklerExample.scala index b2cdc29ed9..055c4375ed 100644 --- a/examples/src/main/scala/sttp/tapir/examples/BooksPicklerExample.scala +++ b/examples/src/main/scala/sttp/tapir/examples/booksPicklerExample.scala @@ -1,12 +1,14 @@ package sttp.tapir.examples -import sttp.tapir.examples.logging.Logging import sttp.tapir.server.netty.{NettyFutureServer, NettyFutureServerBinding} import scala.concurrent.Await import scala.concurrent.duration.Duration -object BooksPicklerExample extends App with Logging { +@main def booksPicklerExample(): Unit = + import org.slf4j.{Logger, LoggerFactory} + val logger: Logger = LoggerFactory.getLogger(getClass.getName) + type Limit = Option[Int] type AuthToken = String @@ -19,9 +21,8 @@ object BooksPicklerExample extends App with Logging { val declaredPort = 9090 val declaredHost = "localhost" - /** Descriptions of endpoints used in the example. - */ - object Endpoints { + /** Descriptions of endpoints used in the example. */ + object Endpoints: import sttp.tapir.* import sttp.tapir.json.pickler.* import sttp.tapir.json.pickler.generic.auto.* @@ -50,10 +51,11 @@ object BooksPicklerExample extends App with Logging { val booksListingByGenre: PublicEndpoint[BooksQuery, String, Vector[Book], Any] = baseEndpoint.get .in(("list" / path[String]("genre").map(Option(_))(_.get)).and(limitParameter).mapTo[BooksQuery]) .out(jsonBody[Vector[Book]]) - } + end Endpoints + // - object Library { + object Library: import java.util.concurrent.atomic.AtomicReference val Books = new AtomicReference( @@ -84,7 +86,7 @@ object BooksPicklerExample extends App with Logging { } filteredBooks } - } + end Library // @@ -93,8 +95,7 @@ object BooksPicklerExample extends App with Logging { import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits.global - def booksServerEndpoints: List[ServerEndpoint[Any, Future]] = { - + def booksServerEndpoints: List[ServerEndpoint[Any, Future]] = def bookAddLogic(book: Book, token: AuthToken): Future[Either[String, Unit]] = Future { if (token != "secret") { @@ -124,24 +125,24 @@ object BooksPicklerExample extends App with Logging { booksListing.serverLogic(bookListingLogic), booksListingByGenre.serverLogic(bookListingByGenreLogic) ) - } + end booksServerEndpoints - def swaggerUIServerEndpoints: List[ServerEndpoint[Any, Future]] = { + def swaggerUIServerEndpoints: List[ServerEndpoint[Any, Future]] = import sttp.tapir.swagger.bundle.SwaggerInterpreter // interpreting the endpoint descriptions as yaml openapi documentation // exposing the docs using SwaggerUI endpoints, interpreted as an akka-http route SwaggerInterpreter().fromEndpoints(List(addBook), "The Tapir Library", "1.0") - } + end swaggerUIServerEndpoints - def makeClientRequest(): Unit = { + def makeClientRequest(): Unit = import sttp.client3.* import sttp.tapir.client.sttp.SttpClientInterpreter val client = SttpClientInterpreter().toQuickClient(booksListing, Some(uri"http://$declaredHost:$declaredPort")) val result: Either[String, Vector[Book]] = client(Some(3)) logger.info("Result of listing request with limit 3: " + result) - } + end makeClientRequest logger.info("Welcome to the Tapir Library example!") @@ -170,4 +171,3 @@ object BooksPicklerExample extends App with Logging { logger.info("Press ENTER to stop the server...") scala.io.StdIn.readLine Await.result(serverBinding.stop(), Duration.Inf) -} 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 096886adc5..076dbd3041 100644 --- a/examples/src/main/scala/sttp/tapir/examples/client/Http4sClientExample.scala +++ b/examples/src/main/scala/sttp/tapir/examples/client/Http4sClientExample.scala @@ -1,14 +1,16 @@ package sttp.tapir.examples.client import cats.effect.{ExitCode, IO, IOApp} -import sttp.tapir.examples.logging.Logging import io.circe.generic.auto.* import sttp.tapir.* import sttp.tapir.client.http4s.Http4sClientInterpreter import sttp.tapir.generic.auto.* import sttp.tapir.json.circe.* -object Http4sClientExample extends IOApp with Logging { +import org.slf4j.{Logger, LoggerFactory} +val logger: Logger = LoggerFactory.getLogger(getClass.getName) + +object Http4sClientExample extends IOApp: case class User(id: Int, name: String) @@ -19,7 +21,7 @@ object Http4sClientExample extends IOApp with Logging { .out(jsonBody[User]) // Define http4s routes that will be used to test the request. - private val http4sRoutes = { + private val http4sRoutes = import io.circe.generic.auto.* import io.circe.syntax.* import org.http4s.* @@ -32,9 +34,8 @@ object Http4sClientExample extends IOApp with Logging { Ok(User(userId, "Joanna").asJson) } .orNotFound - } - override def run(args: List[String]): IO[ExitCode] = { + override def run(args: List[String]): IO[ExitCode] = val userId = 5 // Interpret the endpoint as a request and a response parser. @@ -51,5 +52,3 @@ object Http4sClientExample extends IOApp with Logging { result <- parseResponse(response) _ <- IO(logger.info(s"The result is: $result")) } yield ExitCode.Success - } -} 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 deleted file mode 100644 index 904e37eaa3..0000000000 --- a/examples/src/main/scala/sttp/tapir/examples/custom_types/CommaSeparatedQueryParameter.scala +++ /dev/null @@ -1,80 +0,0 @@ -package sttp.tapir.examples.custom_types - -import sttp.tapir.* -import sttp.tapir.model.{CommaSeparated, Delimited} -import sttp.tapir.server.ServerEndpoint -import sttp.tapir.server.netty.{NettyFutureServer, NettyFutureServerBinding} -import sttp.tapir.swagger.bundle.SwaggerInterpreter - -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.duration.Duration -import scala.concurrent.{Await, Future} - -/* - A simple example that showcases how to use a custom Enumeration as a comma-separated query parameter. - - We're going to build a simple endpoint capable of accepting requests such as - `GET tapirs?breeds=Malayan,CentralAmerican` - - Warning: are you using Scala 2.12 or older? Then you need to write your own `Delimited`, as the one provided - unfortunately only works with Scala 2.13+. - */ -object CommaSeparatedQueryParameter extends App { - - /* - It's possible to use `CommaSeparated` with any type with a `Codec[String, T, CodecFormat.TextPlain]` instance. - - That means, for example - - scala.Enumeration, as in this example - - sealed families (ADTs) - - scala 3 enums - - `Enumeratum` enumerations - - etc. - - That codec will also determine validation rules, e.g. if the input should be case-insensitive or not. - - See also: - - https://tapir.softwaremill.com/en/latest/endpoint/enumerations.html - - https://tapir.softwaremill.com/en/latest/endpoint/integrations.html#enumeratum-integration - */ - object TapirBreeds extends Enumeration { - type Breed = Value - - val CentralAmerican: Breed = Value("Central American") - val SouthAmerican: Breed = Value("South American") - val Mountain: Breed = Value("Mountain") - val Malayan: Breed = Value("Malayan") - } - - val echoTapirs: Endpoint[Unit, CommaSeparated[TapirBreeds.Breed], Unit, String, Any] = endpoint.get - .in("tapirs") - .in( - query[CommaSeparated[TapirBreeds.Breed]]("breeds") - // If we want the filter to be optional, we need either `default` or to make to wrap the parameter in `Optional` - .default(Delimited[",", TapirBreeds.Breed](TapirBreeds.values.toList)) - ) - .out(stringBody) - - val echoTapirsServerEndpoint: ServerEndpoint[Any, Future] = - echoTapirs.serverLogicSuccess[Future](breeds => Future.successful(s"Tapir breeds: ${breeds.values.mkString(", ")}")) - - ////////////////////////////////// Boilerplate: starting up server and swagger UI ////////////////////////////////// - - val docsEndpoints = SwaggerInterpreter().fromServerEndpoints[Future](List(echoTapirsServerEndpoint), "Echo", "1.0.0") - - val serverBinding: NettyFutureServerBinding = - Await.result( - NettyFutureServer() - .port(8080) - .host("localhost") - .addEndpoints(echoTapirsServerEndpoint :: docsEndpoints) - .start(), - Duration.Inf - ) - - println(s"Go to: http://${serverBinding.hostName}:${serverBinding.port}/docs") - println("Press any key to exit ...") - scala.io.StdIn.readLine() - - Await.result(serverBinding.stop(), Duration.Inf) -} 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 c457a9e152..9a244fd469 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 @@ -6,7 +6,7 @@ import sttp.tapir.* import sttp.tapir.generic.auto.* import sttp.tapir.json.circe.* -object EndpointWithCustomTypes { +object EndpointWithCustomTypes: // An over-complicated, example custom type trait MyId { def id: String @@ -28,4 +28,3 @@ object EndpointWithCustomTypes { implicit val myIdEncoder: Encoder[MyId] = Encoder.encodeString.contramap(_.id) implicit val myIdDecoder: Decoder[MyId] = Decoder.decodeString.map(s => new MyIdImpl(s)) val endpointWithPerson: PublicEndpoint[Unit, Unit, Person, Nothing] = endpoint.out(jsonBody[Person]) -} 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 similarity index 93% rename from examples/src/main/scala/sttp/tapir/examples/custom_types/BooksExampleSemiauto.scala rename to examples/src/main/scala/sttp/tapir/examples/custom_types/booksExampleSemiauto.scala index 909a3c6de9..ba442caeec 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,9 +1,11 @@ package sttp.tapir.examples.custom_types -import sttp.tapir.examples.logging.Logging import sttp.tapir.Schema -object BooksExampleSemiauto extends App with Logging { +@main def booksExampleSemiauto(): Unit = + import org.slf4j.{Logger, LoggerFactory} + val logger: Logger = LoggerFactory.getLogger(getClass().getName) + type Limit = Option[Int] type AuthToken = String @@ -21,7 +23,7 @@ object BooksExampleSemiauto extends App with Logging { /** Descriptions of endpoints used in the example. */ - object Endpoints { + object Endpoints: import io.circe.generic.auto.* import sttp.tapir.* import sttp.tapir.json.circe.* @@ -50,11 +52,11 @@ object BooksExampleSemiauto extends App with Logging { val booksListingByGenre: PublicEndpoint[BooksQuery, String, Vector[Book], Any] = baseEndpoint.get .in(("list" / path[String]("genre").map(Option(_))(_.get)).and(limitParameter).mapTo[BooksQuery]) .out(jsonBody[Vector[Book]]) - } + end Endpoints // - object Library { + object Library: import java.util.concurrent.atomic.AtomicReference val Books = new AtomicReference( @@ -85,7 +87,7 @@ object BooksExampleSemiauto extends App with Logging { } filteredBooks } - } + end Library // @@ -94,7 +96,7 @@ object BooksExampleSemiauto extends App with Logging { import scala.concurrent.Future - def booksServerEndpoints: List[ServerEndpoint[Any, Future]] = { + def booksServerEndpoints: List[ServerEndpoint[Any, Future]] = import scala.concurrent.ExecutionContext.Implicits.global def bookAddLogic(book: Book, token: AuthToken): Future[Either[String, Unit]] = @@ -126,17 +128,17 @@ object BooksExampleSemiauto extends App with Logging { booksListing.serverLogic(bookListingLogic), booksListingByGenre.serverLogic(bookListingByGenreLogic) ) - } + end booksServerEndpoints - def swaggerUIServerEndpoints: List[ServerEndpoint[Any, Future]] = { + def swaggerUIServerEndpoints: List[ServerEndpoint[Any, Future]] = import sttp.tapir.swagger.bundle.SwaggerInterpreter // interpreting the endpoint descriptions as yaml openapi documentation // exposing the docs using SwaggerUI endpoints, interpreted as an akka-http route SwaggerInterpreter().fromEndpoints(List(addBook, booksListing, booksListingByGenre), "The Tapir Library", "1.0") - } + end swaggerUIServerEndpoints - def startServer(serverEndpoints: List[ServerEndpoint[Any, Future]]): Unit = { + def startServer(serverEndpoints: List[ServerEndpoint[Any, Future]]): Unit = import org.apache.pekko.actor.ActorSystem import org.apache.pekko.http.scaladsl.Http import sttp.tapir.server.pekkohttp.PekkoHttpServerInterpreter @@ -150,9 +152,9 @@ object BooksExampleSemiauto extends App with Logging { Await.result(Http().newServerAt("localhost", 8080).bindFlow(routes), 1.minute) logger.info("Server started") - } + end startServer - def makeClientRequest(): Unit = { + def makeClientRequest(): Unit = import sttp.client3.* import sttp.tapir.client.sttp.SttpClientInterpreter @@ -161,7 +163,7 @@ object BooksExampleSemiauto extends App with Logging { val result: Either[String, Vector[Book]] = client(Some(3)) logger.info("Result of listing request with limit 3: " + result) - } + end makeClientRequest logger.info("Welcome to the Tapir Library example!") @@ -172,4 +174,3 @@ object BooksExampleSemiauto extends App with Logging { makeClientRequest() logger.info("Try out the API by opening the Swagger UI: http://localhost:8080/docs") -} 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 new file mode 100644 index 0000000000..1a87bfea52 --- /dev/null +++ b/examples/src/main/scala/sttp/tapir/examples/custom_types/commaSeparatedQueryParameter.scala @@ -0,0 +1,73 @@ +package sttp.tapir.examples.custom_types + +import ox.supervised +import sttp.shared.Identity +import sttp.tapir.* +import sttp.tapir.CodecFormat.TextPlain +import sttp.tapir.model.{CommaSeparated, Delimited} +import sttp.tapir.server.netty.sync.NettySyncServer +import sttp.tapir.swagger.bundle.SwaggerInterpreter + +enum TapirBreeds(val name: String): + case CentralAmerican extends TapirBreeds("Central American") + case SouthAmerican extends TapirBreeds("South American") + case Mountain extends TapirBreeds("Mountain") + case Malayan extends TapirBreeds("Malayan") + + override def toString: String = name + +/* + A simple example that showcases how to use a custom `enum` as a comma-separated query parameter. + + We're going to build a simple endpoint capable of accepting requests such as + `GET tapirs?breeds=Malayan,CentralAmerican` + */ +@main def commaSeparatedQueryParameter(): Unit = + /* + It's possible to use `CommaSeparated` with any type with a `Codec[String, T, CodecFormat.TextPlain]` instance. + + That means, for example + - scala.Enumeration, as in this example + - sealed families (ADTs) + - scala 3 enums + - `Enumeratum` enumerations + - etc. + + That codec will also determine validation rules, e.g. if the input should be case-insensitive or not. + + See also: + - https://tapir.softwaremill.com/en/latest/endpoint/enumerations.html + - https://tapir.softwaremill.com/en/latest/endpoint/integrations.html#enumeratum-integration + */ + + given Codec[String, TapirBreeds, TextPlain] = Codec.derivedEnumeration[String, TapirBreeds].defaultStringBased + + val echoTapirs: Endpoint[Unit, CommaSeparated[TapirBreeds], Unit, String, Any] = endpoint.get + .in("tapirs") + .in( + query[CommaSeparated[TapirBreeds]]("breeds") + // If we want the filter to be optional, we need either `default` or to make to wrap the parameter in `Optional` + .default(Delimited[",", TapirBreeds](TapirBreeds.values.toList)) + ) + .out(stringBody) + + val echoTapirsServerEndpoint = + echoTapirs.handleSuccess(breeds => s"Tapir breeds: ${breeds.values.mkString(", ")}") + + ////////////////////////////////// Boilerplate: starting up server and swagger UI ////////////////////////////////// + + val docsEndpoints = SwaggerInterpreter().fromServerEndpoints[Identity](List(echoTapirsServerEndpoint), "Echo", "1.0.0") + + supervised { + val binding = NettySyncServer() + .port(8080) + .host("localhost") + .addEndpoints(echoTapirsServerEndpoint :: docsEndpoints) + .start() + + println(s"Go to: http://${binding.hostName}:${binding.port}/docs") + println("Press any key to exit ...") + scala.io.StdIn.readLine() + + binding.stop() + } 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 new file mode 100644 index 0000000000..9ac37e6386 --- /dev/null +++ b/examples/src/main/scala/sttp/tapir/examples/custom_types/sealedTraitWithDiscriminator.scala @@ -0,0 +1,52 @@ +package sttp.tapir.examples.custom_types + +import io.circe.Codec as CirceCodec +import io.circe.derivation.Configuration as CirceConfiguration +import ox.supervised +import sttp.shared.Identity +import sttp.tapir.* +import sttp.tapir.generic.Configuration +import sttp.tapir.json.circe.* +import sttp.tapir.server.netty.sync.NettySyncServer +import sttp.tapir.swagger.bundle.SwaggerInterpreter + +@main def sealedTraitWithDiscriminator(): Unit = + // data structures + sealed trait Node + case class Leaf(value: String) extends Node + case class Branch(name: String, children: Seq[Node]) extends Node + + val discriminatorFieldName = "kind" + + // these configs must match: one configures tapir's schema, the other circe's encoding/decoding to/from json + given Configuration = Configuration.default.withDiscriminator(discriminatorFieldName) + // the CirceConfiguration class is a renamed import (see above), as the name would clash with tapir's Configuration + given CirceConfiguration = CirceConfiguration.default.withDiscriminator(discriminatorFieldName) + + given CirceCodec[Node] = CirceCodec.AsObject.derivedConfigured + given Schema[Node] = Schema.derived + + // endpoint description; the configs are used when deriving the Schema, Encoder and Decoder, which are implicit + // parameters to jsonBody[Node] + val nodesListing: PublicEndpoint[Unit, Unit, Node, Any] = endpoint.get + .in("nodes") + .out(jsonBody[Node]) + + val nodesListingServerEndpoint = + nodesListing.handleSuccess(_ => Branch("b", List(Leaf("x"), Leaf("y")))) + + val docsEndpoints = SwaggerInterpreter().fromServerEndpoints[Identity](List(nodesListingServerEndpoint), "Nodes", "1.0.0") + + supervised { + val binding = NettySyncServer() + .port(8080) + .host("localhost") + .addEndpoints(nodesListingServerEndpoint :: docsEndpoints) + .start() + + println(s"Go to: http://${binding.hostName}:${binding.port}/docs") + println("Press any key to exit ...") + scala.io.StdIn.readLine() + + binding.stop() + } 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 7889a86572..2a97c02f58 100644 --- a/examples/src/main/scala/sttp/tapir/examples/errors/IronRefinementErrorsNettyServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/errors/IronRefinementErrorsNettyServer.scala @@ -24,7 +24,7 @@ import sttp.tapir.json.circe.* import sttp.tapir.codec.iron.given import sttp.tapir.generic.auto.* -object IronRefinementErrorsNettyServer extends IOApp.Simple { +object IronRefinementErrorsNettyServer extends IOApp.Simple: case class IronException(error: String) extends Exception(error) @@ -81,7 +81,7 @@ object IronRefinementErrorsNettyServer extends IOApp.Simple { private val declaredPort = 9090 private val declaredHost = "localhost" - override def run = NettyCatsServer + override def run: IO[Unit] = NettyCatsServer .io() .use { server => // Don't forget to add the interceptor to server options @@ -123,4 +123,3 @@ object IronRefinementErrorsNettyServer extends IOApp.Simple { .guarantee(binding.stop()) } yield result } -} diff --git a/examples/src/main/scala/sttp/tapir/examples/errors/CustomErrorsOnDecodeFailurePekkoServer.scala b/examples/src/main/scala/sttp/tapir/examples/errors/customErrorsOnDecodeFailurePekkoServer.scala similarity index 96% rename from examples/src/main/scala/sttp/tapir/examples/errors/CustomErrorsOnDecodeFailurePekkoServer.scala rename to examples/src/main/scala/sttp/tapir/examples/errors/customErrorsOnDecodeFailurePekkoServer.scala index 4f41a1746c..07f86310b8 100644 --- a/examples/src/main/scala/sttp/tapir/examples/errors/CustomErrorsOnDecodeFailurePekkoServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/errors/customErrorsOnDecodeFailurePekkoServer.scala @@ -3,6 +3,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} @@ -13,9 +14,7 @@ import sttp.client3.* import sttp.monad.FutureMonad import sttp.tapir.server.interceptor.decodefailure.{DecodeFailureHandler, DefaultDecodeFailureHandler} -import scala.util.{Failure, Success} - -object CustomErrorsOnDecodeFailurePekkoServer extends App { +@main def customErrorsOnDecodeFailurePekkoServer(): Unit = implicit val actorSystem: ActorSystem = ActorSystem() import actorSystem.dispatcher @@ -63,5 +62,4 @@ object CustomErrorsOnDecodeFailurePekkoServer extends App { binding } - Await.result(bindAndCheck.flatMap(_.terminate(1.minute)), 1.minute) -} + Await.result(bindAndCheck.flatMap(_.terminate(1.minute)), 1.minute).discard diff --git a/examples/src/main/scala/sttp/tapir/examples/errors/ErrorOutputsPekkoServer.scala b/examples/src/main/scala/sttp/tapir/examples/errors/errorOutputsPekkoServer.scala similarity index 95% rename from examples/src/main/scala/sttp/tapir/examples/errors/ErrorOutputsPekkoServer.scala rename to examples/src/main/scala/sttp/tapir/examples/errors/errorOutputsPekkoServer.scala index f4ebff48e4..7a0972c377 100644 --- a/examples/src/main/scala/sttp/tapir/examples/errors/ErrorOutputsPekkoServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/errors/errorOutputsPekkoServer.scala @@ -3,6 +3,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.* @@ -14,7 +15,7 @@ import io.circe.generic.auto.* import scala.concurrent.duration.* import scala.concurrent.{Await, Future} -object ErrorOutputsPekkoServer extends App { +@main def errorOutputsPekkoServer(): Unit = implicit val actorSystem: ActorSystem = ActorSystem() import actorSystem.dispatcher @@ -49,5 +50,4 @@ object ErrorOutputsPekkoServer extends App { binding } - Await.result(bindAndCheck.flatMap(_.terminate(1.minute)), 1.minute) -} + Await.result(bindAndCheck.flatMap(_.terminate(1.minute)), 1.minute).discard diff --git a/examples/src/main/scala/sttp/tapir/examples/HelloWorldArmeriaServer.scala b/examples/src/main/scala/sttp/tapir/examples/helloWorldArmeriaServer.scala similarity index 96% rename from examples/src/main/scala/sttp/tapir/examples/HelloWorldArmeriaServer.scala rename to examples/src/main/scala/sttp/tapir/examples/helloWorldArmeriaServer.scala index 7e14d83549..bfadc347f5 100644 --- a/examples/src/main/scala/sttp/tapir/examples/HelloWorldArmeriaServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/helloWorldArmeriaServer.scala @@ -9,7 +9,7 @@ import sttp.tapir.* import scala.concurrent.Future -object HelloWorldArmeriaServer extends App { +@main def helloWorldArmeriaServer(): Unit = // the endpoint: single fixed path input ("hello"), single query parameter // corresponds to: GET /hello?name=... @@ -35,4 +35,3 @@ object HelloWorldArmeriaServer extends App { assert(result == "Hello, Frodo!") server.stop().join() -} diff --git a/examples/src/main/scala/sttp/tapir/examples/HelloWorldJdkHttpServer.scala b/examples/src/main/scala/sttp/tapir/examples/helloWorldJdkHttpServer.scala similarity index 95% rename from examples/src/main/scala/sttp/tapir/examples/HelloWorldJdkHttpServer.scala rename to examples/src/main/scala/sttp/tapir/examples/helloWorldJdkHttpServer.scala index 4aa2a27205..d466dd7b6f 100644 --- a/examples/src/main/scala/sttp/tapir/examples/HelloWorldJdkHttpServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/helloWorldJdkHttpServer.scala @@ -6,8 +6,7 @@ import sttp.shared.Identity import sttp.tapir.server.jdkhttp.* import sttp.tapir.* -object HelloWorldJdkHttpServer extends App { - +@main def helloWorldJdkHttpServer(): Unit = // GET /hello endpoint, with query parameter `name` val helloWorldEndpoint: PublicEndpoint[String, Unit, String, Any] = endpoint.get.in("hello").in(query[String]("name")).out(stringBody) @@ -20,8 +19,8 @@ object HelloWorldJdkHttpServer extends App { val secondServerEndpoint = secondEndpoint.handleSuccess(_ => "IT WORKS!") - private val declaredPort = 9090 - private val declaredHost = "localhost" + val declaredPort = 9090 + val declaredHost = "localhost" // Starting jdk http server val server = @@ -60,4 +59,3 @@ object HelloWorldJdkHttpServer extends App { } finally { server.stop(0) } -} diff --git a/examples/src/main/scala/sttp/tapir/examples/HelloWorldNettyFutureServer.scala b/examples/src/main/scala/sttp/tapir/examples/helloWorldNettyFutureServer.scala similarity index 94% rename from examples/src/main/scala/sttp/tapir/examples/HelloWorldNettyFutureServer.scala rename to examples/src/main/scala/sttp/tapir/examples/helloWorldNettyFutureServer.scala index 1f3be12861..efd8a53ac2 100644 --- a/examples/src/main/scala/sttp/tapir/examples/HelloWorldNettyFutureServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/helloWorldNettyFutureServer.scala @@ -10,7 +10,7 @@ import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration.Duration import scala.concurrent.{Await, Future} -object HelloWorldNettyFutureServer extends App { +@main def helloWorldNettyFutureServer(): Unit = // One endpoint on GET /hello with query parameter `name` val helloWorldEndpoint: PublicEndpoint[String, Unit, String, Any] = endpoint.get.in("hello").in(query[String]("name")).out(stringBody) @@ -19,8 +19,9 @@ object HelloWorldNettyFutureServer extends App { val helloWorldServerEndpoint = helloWorldEndpoint .serverLogic(name => Future.successful[Either[Unit, String]](Right(s"Hello, $name!"))) - private val declaredPort = 9090 - private val declaredHost = "localhost" + val declaredPort = 9090 + val declaredHost = "localhost" + // Starting netty server val serverBinding: NettyFutureServerBinding = Await.result( @@ -53,4 +54,3 @@ object HelloWorldNettyFutureServer extends App { assert(host == declaredHost, "Hosts don't match") Await.result(serverBinding.stop(), Duration.Inf) -} diff --git a/examples/src/main/scala/sttp/tapir/examples/HelloWorldNettySyncServer.scala b/examples/src/main/scala/sttp/tapir/examples/helloWorldNettySyncServer.scala similarity index 90% rename from examples/src/main/scala/sttp/tapir/examples/HelloWorldNettySyncServer.scala rename to examples/src/main/scala/sttp/tapir/examples/helloWorldNettySyncServer.scala index 0580ebaab2..537d176bd4 100644 --- a/examples/src/main/scala/sttp/tapir/examples/HelloWorldNettySyncServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/helloWorldNettySyncServer.scala @@ -4,7 +4,7 @@ import ox.* import sttp.tapir.* import sttp.tapir.server.netty.sync.NettySyncServer -object HelloWorldNettySyncServer: +@main def helloWorldNettySyncServer(): Unit = val helloWorld = endpoint.get .in("hello") .in(query[String]("name")) @@ -15,7 +15,7 @@ object HelloWorldNettySyncServer: // Alternatively, if you need manual control of the structured concurrency scope, server lifecycle, // or just metadata from `NettySyncServerBinding` (like port number), use `start()`: -object HelloWorldNettySyncServer2: +@main def helloWorldNettySyncServer2(): Unit = val helloWorld = endpoint.get .in("hello") .in(query[String]("name")) diff --git a/examples/src/main/scala/sttp/tapir/examples/HelloWorldPekkoServer.scala b/examples/src/main/scala/sttp/tapir/examples/helloWorldPekkoServer.scala similarity index 97% rename from examples/src/main/scala/sttp/tapir/examples/HelloWorldPekkoServer.scala rename to examples/src/main/scala/sttp/tapir/examples/helloWorldPekkoServer.scala index 95f6ff5bed..b5fe8e1603 100644 --- a/examples/src/main/scala/sttp/tapir/examples/HelloWorldPekkoServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/helloWorldPekkoServer.scala @@ -11,7 +11,7 @@ import sttp.tapir.server.pekkohttp.PekkoHttpServerInterpreter import scala.concurrent.duration.* import scala.concurrent.{Await, Future} -object HelloWorldPekkoServer extends App { +@main def helloWorldPekkoServer(): Unit = implicit val actorSystem: ActorSystem = ActorSystem() import actorSystem.dispatcher @@ -37,4 +37,3 @@ object HelloWorldPekkoServer extends App { } Await.result(bindAndCheck.flatMap(_.terminate(1.minute)), 1.minute) -} diff --git a/examples/src/main/scala/sttp/tapir/examples/logging/Logging.scala b/examples/src/main/scala/sttp/tapir/examples/logging/Logging.scala deleted file mode 100644 index fe33bf4dc4..0000000000 --- a/examples/src/main/scala/sttp/tapir/examples/logging/Logging.scala +++ /dev/null @@ -1,12 +0,0 @@ -package sttp.tapir.examples.logging - -import org.slf4j.{Logger, LoggerFactory} - -/** Defines a [[org.slf4j.Logger]] instance `logger` named according to the class into which this trait is mixed. - * - * In a real-life project, you might rather want to use a macros-based SLF4J wrapper or logging backend. - */ -trait Logging { - - protected val logger: Logger = LoggerFactory.getLogger(getClass.getName) -} 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 fdfa3a0adc..4d75eeac56 100644 --- a/examples/src/main/scala/sttp/tapir/examples/logging/ZioLoggingWithCorrelationIdNettyServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/logging/ZioLoggingWithCorrelationIdNettyServer.scala @@ -9,7 +9,7 @@ import sttp.tapir.server.netty.zio.{NettyZioServer, NettyZioServerOptions} import sttp.tapir.ztapir.* import zio.{ExitCode, Task, URIO, ZIO, ZIOAppDefault, durationInt} -object ZioLoggingWithCorrelationIdNettyServer extends ZIOAppDefault { +object ZioLoggingWithCorrelationIdNettyServer extends ZIOAppDefault: val CorrelationIdHeader = "X-Correlation-Id" // An endpoint with some logging added @@ -30,7 +30,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() @@ -39,5 +39,3 @@ object ZioLoggingWithCorrelationIdNettyServer extends ZIOAppDefault { _ <- httpClient.send(basicRequest.get(uri"http://localhost:8080/hello?name=Bob")) _ <- binding.stop() } yield ()).exitCode - } -} diff --git a/examples/src/main/scala/sttp/tapir/examples/multipart/MultipartFormUploadPekkoServer.scala b/examples/src/main/scala/sttp/tapir/examples/multipart/multipartFormUploadPekkoServer.scala similarity index 96% rename from examples/src/main/scala/sttp/tapir/examples/multipart/MultipartFormUploadPekkoServer.scala rename to examples/src/main/scala/sttp/tapir/examples/multipart/multipartFormUploadPekkoServer.scala index d978daa7fd..424157c8d8 100644 --- a/examples/src/main/scala/sttp/tapir/examples/multipart/MultipartFormUploadPekkoServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/multipart/multipartFormUploadPekkoServer.scala @@ -5,6 +5,7 @@ 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.* @@ -15,7 +16,7 @@ import sttp.tapir.server.pekkohttp.PekkoHttpServerInterpreter import scala.concurrent.{Await, Future} import scala.concurrent.duration.* -object MultipartFormUploadPekkoServer extends App { +@main def multipartFormUploadPekkoServer(): Unit = implicit val actorSystem: ActorSystem = ActorSystem() import actorSystem.dispatcher @@ -61,5 +62,4 @@ object MultipartFormUploadPekkoServer extends App { binding } - Await.result(bindAndCheck.flatMap(_.terminate(1.minute)), 1.minute) -} + Await.result(bindAndCheck.flatMap(_.terminate(1.minute)), 1.minute).discard 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 7c1334a6b2..06260ce091 100644 --- a/examples/src/main/scala/sttp/tapir/examples/observability/ZioMetricsExample.scala +++ b/examples/src/main/scala/sttp/tapir/examples/observability/ZioMetricsExample.scala @@ -9,7 +9,7 @@ import zio.http.{Response => ZioHttpResponse, Routes, Server} import zio.{Task, ZIO, _} /** Based on https://adopt-tapir.softwaremill.com zio version. */ -object ZioMetricsExample extends ZIOAppDefault { +object ZioMetricsExample extends ZIOAppDefault: case class User(name: String) extends AnyVal @@ -27,7 +27,7 @@ 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 with ZIOAppArgs with Scope, Any, Any] = val serverOptions: ZioHttpServerOptions[Any] = ZioHttpServerOptions.customiseInterceptors.metricsInterceptor(metricsInterceptor).options val app: Routes[Any, ZioHttpResponse] = ZioHttpInterpreter(serverOptions).toHttp(all) @@ -44,6 +44,3 @@ object ZioMetricsExample extends ZIOAppDefault { Server.live ) .exitCode - } - -} diff --git a/examples/src/main/scala/sttp/tapir/examples/observability/DatadogMetricsExample.scala b/examples/src/main/scala/sttp/tapir/examples/observability/datadogMetricsExample.scala similarity index 93% rename from examples/src/main/scala/sttp/tapir/examples/observability/DatadogMetricsExample.scala rename to examples/src/main/scala/sttp/tapir/examples/observability/datadogMetricsExample.scala index 89fff38076..cf487b2105 100644 --- a/examples/src/main/scala/sttp/tapir/examples/observability/DatadogMetricsExample.scala +++ b/examples/src/main/scala/sttp/tapir/examples/observability/datadogMetricsExample.scala @@ -1,7 +1,6 @@ package sttp.tapir.examples.observability import com.timgroup.statsd.NonBlockingStatsDClientBuilder -import sttp.tapir.examples.logging.Logging import io.circe.generic.auto.* import sttp.tapir.* import sttp.tapir.generic.auto.* @@ -15,8 +14,10 @@ import scala.concurrent.duration.* import scala.concurrent.{Await, Future} import scala.io.StdIn -object DatadogMetricsExample extends App with Logging { +import org.slf4j.{Logger, LoggerFactory} +val logger: Logger = LoggerFactory.getLogger(getClass.getName) +@main def datadogMetricsExample(): Unit = case class Person(name: String) // Simple endpoint returning 200 or 400 response with string body @@ -61,4 +62,3 @@ object DatadogMetricsExample extends App with Logging { } yield stop Await.result(program, Duration.Inf) -} diff --git a/examples/src/main/scala/sttp/tapir/examples/observability/OpenTelemetryMetricsExample.scala b/examples/src/main/scala/sttp/tapir/examples/observability/openTelemetryMetricsExample.scala similarity index 96% rename from examples/src/main/scala/sttp/tapir/examples/observability/OpenTelemetryMetricsExample.scala rename to examples/src/main/scala/sttp/tapir/examples/observability/openTelemetryMetricsExample.scala index aec1dc1a35..a859631b39 100644 --- a/examples/src/main/scala/sttp/tapir/examples/observability/OpenTelemetryMetricsExample.scala +++ b/examples/src/main/scala/sttp/tapir/examples/observability/openTelemetryMetricsExample.scala @@ -1,6 +1,5 @@ package sttp.tapir.examples.observability -import sttp.tapir.examples.logging.Logging import io.circe.generic.auto.* import io.opentelemetry.api.OpenTelemetry import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter @@ -57,7 +56,9 @@ import scala.io.StdIn * ... * }}} */ -object OpenTelemetryMetricsExample extends App with Logging { +@main def openTelemetryMetricsExample(): Unit = + import org.slf4j.{Logger, LoggerFactory} + val logger: Logger = LoggerFactory.getLogger(getClass().getName) case class Person(name: String) @@ -104,4 +105,3 @@ object OpenTelemetryMetricsExample extends App with Logging { } yield stop Await.result(program, Duration.Inf) -} diff --git a/examples/src/main/scala/sttp/tapir/examples/observability/PrometheusMetricsExample.scala b/examples/src/main/scala/sttp/tapir/examples/observability/prometheusMetricsExample.scala similarity index 92% rename from examples/src/main/scala/sttp/tapir/examples/observability/PrometheusMetricsExample.scala rename to examples/src/main/scala/sttp/tapir/examples/observability/prometheusMetricsExample.scala index e5933a4a39..76b142ecce 100644 --- a/examples/src/main/scala/sttp/tapir/examples/observability/PrometheusMetricsExample.scala +++ b/examples/src/main/scala/sttp/tapir/examples/observability/prometheusMetricsExample.scala @@ -1,6 +1,5 @@ package sttp.tapir.examples.observability -import sttp.tapir.examples.logging.Logging import io.circe.generic.auto.* import sttp.tapir.* import sttp.tapir.generic.auto.* @@ -14,7 +13,9 @@ import scala.concurrent.duration.* import scala.concurrent.{Await, Future} import scala.io.StdIn -object PrometheusMetricsExample extends App with Logging { +@main def prometheusMetricsExample(): Unit = + import org.slf4j.{Logger, LoggerFactory} + val logger: Logger = LoggerFactory.getLogger(getClass().getName) case class Person(name: String) @@ -54,4 +55,3 @@ object PrometheusMetricsExample extends App with Logging { } yield stop Await.result(program, Duration.Inf) -} 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 fb035e8cf2..b954caba00 100644 --- a/examples/src/main/scala/sttp/tapir/examples/openapi/MultipleEndpointsDocumentationHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/openapi/MultipleEndpointsDocumentationHttp4sServer.scala @@ -15,7 +15,7 @@ import sttp.tapir.swagger.bundle.SwaggerInterpreter import java.util.concurrent.atomic.AtomicReference import scala.concurrent.ExecutionContext -object MultipleEndpointsDocumentationHttp4sServer extends IOApp { +object MultipleEndpointsDocumentationHttp4sServer extends IOApp: // endpoint descriptions case class Author(name: String) case class Book(title: String, year: Int, author: Author) @@ -63,7 +63,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,5 +78,4 @@ 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 d250989951..0addec9329 100644 --- a/examples/src/main/scala/sttp/tapir/examples/openapi/RedocContextPathHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/openapi/RedocContextPathHttp4sServer.scala @@ -12,23 +12,23 @@ import sttp.tapir.server.http4s.Http4sServerInterpreter import scala.concurrent.ExecutionContext -object RedocContextPathHttp4sServer extends IOApp { +object RedocContextPathHttp4sServer extends IOApp: val contextPath: List[String] = List("api", "v1") val docPathPrefix: List[String] = "redoc" :: Nil val helloWorld: PublicEndpoint[String, Unit, String, Any] = endpoint.get.in("hello").in(query[String]("name")).out(stringBody) - val routes: HttpRoutes[IO] = { + val routes: HttpRoutes[IO] = val redocEndpoints = RedocInterpreter(redocUIOptions = RedocUIOptions.default.contextPath(contextPath).pathPrefix(docPathPrefix)) .fromEndpoints[IO](List(helloWorld), "The tapir library", "1.0.0") Http4sServerInterpreter[IO]().toRoutes(helloWorld.serverLogic(name => IO(s"Hello, $name!".asRight[Unit])) :: redocEndpoints) - } + end routes implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global - override def run(args: List[String]): IO[ExitCode] = { + override def run(args: List[String]): IO[ExitCode] = // starting the server BlazeServerBuilder[IO] .withExecutionContext(ec) @@ -37,5 +37,3 @@ object RedocContextPathHttp4sServer extends IOApp { .resource .use { _ => IO.println(s"go to: http://127.0.0.1:8080/${(contextPath ++ docPathPrefix).mkString("/")}") *> IO.never } .as(ExitCode.Success) - } -} 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 29c1dd5444..517bcbb153 100644 --- a/examples/src/main/scala/sttp/tapir/examples/openapi/RedocZioHttpServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/openapi/RedocZioHttpServer.scala @@ -10,7 +10,7 @@ import zio.http.{Response => ZioHttpResponse, Routes, Server} import zio.Console.{printLine, readLine} import zio.{Task, ZIO, ZIOAppDefault, ZLayer} -object RedocZioHttpServer extends ZIOAppDefault { +object RedocZioHttpServer extends ZIOAppDefault: case class Pet(species: String, url: String) val petEndpoint: ZServerEndpoint[Any, Any] = @@ -24,7 +24,7 @@ object RedocZioHttpServer extends ZIOAppDefault { val redocRoutes: Routes[Any, ZioHttpResponse] = ZioHttpInterpreter().toHttp(RedocInterpreter().fromServerEndpoints[Task](List(petEndpoint), "Our pets", "1.0")) - val app = (petRoutes ++ redocRoutes) + val app: Routes[Any, ZioHttpResponse] = (petRoutes ++ redocRoutes) override def run = { printLine("Go to: http://localhost:8080/docs") *> @@ -40,4 +40,3 @@ object RedocZioHttpServer extends ZIOAppDefault { readLine *> fiber.interrupt } }.exitCode -} diff --git a/examples/src/main/scala/sttp/tapir/examples/openapi/MultipleEndpointsDocumentationPekkoServer.scala b/examples/src/main/scala/sttp/tapir/examples/openapi/multipleEndpointsDocumentationPekkoServer.scala similarity index 96% rename from examples/src/main/scala/sttp/tapir/examples/openapi/MultipleEndpointsDocumentationPekkoServer.scala rename to examples/src/main/scala/sttp/tapir/examples/openapi/multipleEndpointsDocumentationPekkoServer.scala index 8e64d222a3..c45cf28c0f 100644 --- a/examples/src/main/scala/sttp/tapir/examples/openapi/MultipleEndpointsDocumentationPekkoServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/openapi/multipleEndpointsDocumentationPekkoServer.scala @@ -3,6 +3,7 @@ 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.* @@ -13,7 +14,7 @@ import sttp.tapir.swagger.bundle.SwaggerInterpreter import scala.concurrent.duration.* import scala.concurrent.{Await, Future} -object MultipleEndpointsDocumentationPekkoServer extends App { +@main def multipleEndpointsDocumentationPekkoServer(): Unit = implicit val actorSystem: ActorSystem = ActorSystem() import actorSystem.dispatcher @@ -75,5 +76,4 @@ object MultipleEndpointsDocumentationPekkoServer extends App { } // cleanup - Await.result(bindAndCheck.flatMap(_.terminate(1.minute)), 1.minute) -} + Await.result(bindAndCheck.flatMap(_.terminate(1.minute)), 1.minute).discard diff --git a/examples/src/main/scala/sttp/tapir/examples/openapi/OpenapiExtensions.scala b/examples/src/main/scala/sttp/tapir/examples/openapi/openapiExtensions.scala similarity index 96% rename from examples/src/main/scala/sttp/tapir/examples/openapi/OpenapiExtensions.scala rename to examples/src/main/scala/sttp/tapir/examples/openapi/openapiExtensions.scala index 3e975f80a9..d8e70756d7 100644 --- a/examples/src/main/scala/sttp/tapir/examples/openapi/OpenapiExtensions.scala +++ b/examples/src/main/scala/sttp/tapir/examples/openapi/openapiExtensions.scala @@ -10,7 +10,7 @@ import sttp.tapir.docs.apispec.DocsExtensionAttribute.* import sttp.tapir.generic.auto.* import sttp.tapir.json.circe.* -object OpenapiExtensions extends App { +@main def openapiExtensions(): Unit = case class Sample(foo: Boolean, bar: String, baz: Int) @@ -35,4 +35,3 @@ object OpenapiExtensions extends App { val openapi = OpenAPIDocsInterpreter().toOpenAPI(sampleEndpoint, Info("title", "1.0"), rootExtensions) println(openapi.toYaml) -} diff --git a/examples/src/main/scala/sttp/tapir/examples/openapi/SwaggerUIOAuth2PekkoServer.scala b/examples/src/main/scala/sttp/tapir/examples/openapi/swaggerUIOAuth2PekkoServer.scala similarity index 94% rename from examples/src/main/scala/sttp/tapir/examples/openapi/SwaggerUIOAuth2PekkoServer.scala rename to examples/src/main/scala/sttp/tapir/examples/openapi/swaggerUIOAuth2PekkoServer.scala index 01fcc65c0c..4ec59601b2 100644 --- a/examples/src/main/scala/sttp/tapir/examples/openapi/SwaggerUIOAuth2PekkoServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/openapi/swaggerUIOAuth2PekkoServer.scala @@ -2,7 +2,8 @@ package sttp.tapir.examples.openapi import org.apache.pekko.actor.ActorSystem import org.apache.pekko.http.scaladsl.Http -import org.apache.pekko.http.scaladsl.server.{Route, RouteConcatenation} +import org.apache.pekko.http.scaladsl.server.Route +import org.apache.pekko.http.scaladsl.server.Directives.* import sttp.tapir.* import sttp.tapir.server.PartialServerEndpoint import sttp.tapir.server.pekkohttp.PekkoHttpServerInterpreter @@ -28,7 +29,7 @@ import scala.concurrent.{Await, Future, Promise} * * Go to: [[http://localhost:3333/docs]] And try authorize by using `Authorize` by providing details of clients and user */ -object SwaggerUIOAuth2PekkoServer extends App with RouteConcatenation { +@main def swaggerUIOAuth2PekkoServer(): Unit = implicit val actorSystem: ActorSystem = ActorSystem() import actorSystem.dispatcher @@ -71,4 +72,3 @@ object SwaggerUIOAuth2PekkoServer extends App with RouteConcatenation { val promise = Promise[Unit]() Await.result(promise.future, 100.minutes) -} diff --git a/examples/src/main/scala/sttp/tapir/examples/schema/CustomisingSchemas.scala b/examples/src/main/scala/sttp/tapir/examples/schema/customisingSchemas.scala similarity index 98% rename from examples/src/main/scala/sttp/tapir/examples/schema/CustomisingSchemas.scala rename to examples/src/main/scala/sttp/tapir/examples/schema/customisingSchemas.scala index 9ed981f9df..af95ba150b 100644 --- a/examples/src/main/scala/sttp/tapir/examples/schema/CustomisingSchemas.scala +++ b/examples/src/main/scala/sttp/tapir/examples/schema/customisingSchemas.scala @@ -14,7 +14,7 @@ import scala.concurrent.duration.Duration import scala.concurrent.{Await, Future} /** @see https://tapir.softwaremill.com/en/v1.0.5/endpoint/schemas.html#schema-derivation */ -object CustomisingSchemas extends App { +@main def customisingSchemas(): Unit = // schema customised using annotations val dragonAgeValidator = Validator.min(100) case class Dragon(@description("how people refer to the dragon") name: String, @validateEach(dragonAgeValidator) age: Option[Int]) @@ -59,4 +59,3 @@ object CustomisingSchemas extends App { scala.io.StdIn.readLine() Await.result(serverBinding.stop(), Duration.Inf) -} 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 a3a2482f22..53e6b747cf 100644 --- a/examples/src/main/scala/sttp/tapir/examples/security/OAuth2GithubHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/security/OAuth2GithubHttp4sServer.scala @@ -19,7 +19,7 @@ import java.time.Instant import scala.collection.immutable.ListMap import scala.concurrent.ExecutionContext -object OAuth2GithubHttp4sServer extends IOApp { +object OAuth2GithubHttp4sServer extends IOApp: implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global @@ -105,7 +105,7 @@ object OAuth2GithubHttp4sServer extends IOApp { val httpClient = AsyncHttpClientCatsBackend.resource[IO]() - override def run(args: List[String]): IO[ExitCode] = { + override def run(args: List[String]): IO[ExitCode] = // starting the server httpClient .use(backend => @@ -123,5 +123,3 @@ object OAuth2GithubHttp4sServer extends IOApp { } ) .as(ExitCode.Success) - } -} diff --git a/examples2/src/main/scala/sttp/tapir/examples2/security/ServerSecurityLogicZio.scala b/examples/src/main/scala/sttp/tapir/examples/security/ServerSecurityLogicZio.scala similarity index 95% rename from examples2/src/main/scala/sttp/tapir/examples2/security/ServerSecurityLogicZio.scala rename to examples/src/main/scala/sttp/tapir/examples/security/ServerSecurityLogicZio.scala index 37c41191e2..bd42b8413b 100644 --- a/examples2/src/main/scala/sttp/tapir/examples2/security/ServerSecurityLogicZio.scala +++ b/examples/src/main/scala/sttp/tapir/examples/security/ServerSecurityLogicZio.scala @@ -1,4 +1,4 @@ -package sttp.tapir.examples2.security +package sttp.tapir.examples.security import sttp.client3._ import sttp.client3.asynchttpclient.zio.AsyncHttpClientZioBackend @@ -8,7 +8,7 @@ import sttp.tapir.ztapir._ import zio.http.{Response => ZioHttpResponse, Routes, Server} import zio.{Console, ExitCode, IO, Scope, Task, ZIO, ZIOAppDefault, ZLayer} -object ServerSecurityLogicZio extends ZIOAppDefault { +object ServerSecurityLogicZio extends ZIOAppDefault: // authentication data structure & logic case class User(name: String) case class AuthenticationToken(value: String) @@ -57,7 +57,7 @@ object ServerSecurityLogicZio extends ZIOAppDefault { // interpreting as an app val routes: Routes[Any, ZioHttpResponse] = ZioHttpInterpreter().toHttp(secureHelloWorldWithLogic) - override def run: ZIO[Scope, Throwable, ExitCode] = { + override def run: ZIO[Scope, Throwable, ExitCode] = def testWith(backend: SttpBackend[Task, Any], port: Int, path: String, salutation: String, token: String): Task[String] = basicRequest .response(asStringAlways) @@ -75,9 +75,7 @@ object ServerSecurityLogicZio extends ZIOAppDefault { _ <- testWith(backend, port, "hello", "Hello", "apple").map(r => assert(r == "1001")) _ <- testWith(backend, port, "hello", "Hello", "smurf").map(r => assert(r == "Not saying hello to Gargamel!")) } yield ()).exitCode - .provideSome( + .provideSome[Scope]( ZLayer.succeed(Server.Config.default.port(8080)), Server.live ) - } -} diff --git a/examples/src/main/scala/sttp/tapir/examples/security/BasicAuthenticationPekkoServer.scala b/examples/src/main/scala/sttp/tapir/examples/security/basicAuthenticationPekkoServer.scala similarity index 97% rename from examples/src/main/scala/sttp/tapir/examples/security/BasicAuthenticationPekkoServer.scala rename to examples/src/main/scala/sttp/tapir/examples/security/basicAuthenticationPekkoServer.scala index f5fb7c9726..dc7843a681 100644 --- a/examples/src/main/scala/sttp/tapir/examples/security/BasicAuthenticationPekkoServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/security/basicAuthenticationPekkoServer.scala @@ -14,7 +14,7 @@ import sttp.tapir.server.pekkohttp.* import scala.concurrent.duration.* import scala.concurrent.{Await, Future} -object BasicAuthenticationPekkoServer extends App { +@main def basicAuthenticationPekkoServer(): Unit = implicit val actorSystem: ActorSystem = ActorSystem() import actorSystem.dispatcher @@ -46,4 +46,3 @@ object BasicAuthenticationPekkoServer extends App { } 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 similarity index 97% rename from examples/src/main/scala/sttp/tapir/examples/security/CorsInterceptorPekkoServer.scala rename to examples/src/main/scala/sttp/tapir/examples/security/corsInterceptorPekkoServer.scala index b761bfe3ba..e50d3924b0 100644 --- a/examples/src/main/scala/sttp/tapir/examples/security/CorsInterceptorPekkoServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/security/corsInterceptorPekkoServer.scala @@ -6,7 +6,6 @@ import org.apache.pekko.http.scaladsl.server.Route import sttp.client3.* import sttp.model.{Header, HeaderNames, Method, StatusCode} -import scala.util.{Failure, Success} import sttp.model.headers.Origin import sttp.tapir.* import sttp.tapir.server.interceptor.cors.{CORSConfig, CORSInterceptor} @@ -15,7 +14,7 @@ import sttp.tapir.server.pekkohttp.* import scala.concurrent.duration.* import scala.concurrent.{Await, Future} -object CorsInterceptorPekkoServer extends App { +@main def corsInterceptorPekkoServer(): Unit = implicit val actorSystem: ActorSystem = ActorSystem() import actorSystem.dispatcher @@ -94,4 +93,3 @@ object CorsInterceptorPekkoServer extends App { } Await.result(bindAndCheck.flatMap(_.terminate(1.minute)), 1.minute) -} diff --git a/examples/src/main/scala/sttp/tapir/examples/security/ExternalSecurityInterceptor.scala b/examples/src/main/scala/sttp/tapir/examples/security/externalSecurityInterceptor.scala similarity index 98% rename from examples/src/main/scala/sttp/tapir/examples/security/ExternalSecurityInterceptor.scala rename to examples/src/main/scala/sttp/tapir/examples/security/externalSecurityInterceptor.scala index c00dd3cb12..e68eaa7775 100644 --- a/examples/src/main/scala/sttp/tapir/examples/security/ExternalSecurityInterceptor.scala +++ b/examples/src/main/scala/sttp/tapir/examples/security/externalSecurityInterceptor.scala @@ -26,7 +26,7 @@ import scala.concurrent.duration.Duration * (using an attribute), which are allowed to access the endpoint. Verification, if the endpoint can be accessed is done using an * [[EndpointInterceptor]]. */ -object ExternalSecurityInterceptor extends App { +@main def externalSecurityInterceptor(): Unit = // the sidecar/gateway should add the authenticated role in this header val roleHeader = "X-Role" @@ -104,4 +104,3 @@ object ExternalSecurityInterceptor extends App { assert(secretRequest("2").header("X-Role", "role2").send(backend).code == StatusCode(200)) assert(secretRequest("2").header("X-Role", "role3").send(backend).code == StatusCode(200)) } finally Await.result(serverBinding.stop(), Duration.Inf) -} diff --git a/examples/src/main/scala/sttp/tapir/examples/security/ServerSecurityLogicPekko.scala b/examples/src/main/scala/sttp/tapir/examples/security/serverSecurityLogicPekko.scala similarity index 98% rename from examples/src/main/scala/sttp/tapir/examples/security/ServerSecurityLogicPekko.scala rename to examples/src/main/scala/sttp/tapir/examples/security/serverSecurityLogicPekko.scala index d2e3b1185e..956cc89ced 100644 --- a/examples/src/main/scala/sttp/tapir/examples/security/ServerSecurityLogicPekko.scala +++ b/examples/src/main/scala/sttp/tapir/examples/security/serverSecurityLogicPekko.scala @@ -14,7 +14,7 @@ import sttp.tapir.server.pekkohttp.PekkoHttpServerInterpreter import scala.concurrent.duration.* import scala.concurrent.{Await, Future} -object ServerSecurityLogicPekko extends App { +@main def serverSecurityLogicPekko(): Unit = implicit val actorSystem: ActorSystem = ActorSystem() import actorSystem.dispatcher @@ -89,4 +89,3 @@ object ServerSecurityLogicPekko extends App { } 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 similarity index 97% rename from examples/src/main/scala/sttp/tapir/examples/security/ServerSecurityLogicRefreshCookiesPekko.scala rename to examples/src/main/scala/sttp/tapir/examples/security/serverSecurityLogicRefreshCookiesPekko.scala index 22b2369e67..50fa7e67cf 100644 --- a/examples/src/main/scala/sttp/tapir/examples/security/ServerSecurityLogicRefreshCookiesPekko.scala +++ b/examples/src/main/scala/sttp/tapir/examples/security/serverSecurityLogicRefreshCookiesPekko.scala @@ -15,7 +15,7 @@ import sttp.tapir.server.{PartialServerEndpointWithSecurityOutput, ServerEndpoin import scala.concurrent.duration.* import scala.concurrent.{Await, Future} -object ServerSecurityLogicRefreshCookiesPekko extends App { +@main def serverSecurityLogicRefreshCookiesPekko(): Unit = implicit val actorSystem: ActorSystem = ActorSystem() import actorSystem.dispatcher @@ -70,4 +70,3 @@ object ServerSecurityLogicRefreshCookiesPekko extends App { } 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 deleted file mode 100644 index d1e80ef4f9..0000000000 --- a/examples/src/main/scala/sttp/tapir/examples/static_content/StaticContentFromFilesNettyServer.scala +++ /dev/null @@ -1,16 +0,0 @@ -package sttp.tapir.examples.static_content - -import sttp.tapir.server.netty.NettyFutureServer -import sttp.tapir.emptyInput -import sttp.tapir.files.* - -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future - -object StaticContentFromFilesNettyServer extends App { - NettyFutureServer() - .port(8080) - .addEndpoints(staticFilesServerEndpoints[Future](emptyInput)("/var/www")) - .start() - .flatMap(_ => Future.never) -} 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 new file mode 100644 index 0000000000..df286a76bd --- /dev/null +++ b/examples/src/main/scala/sttp/tapir/examples/static_content/staticContentFromFilesNettyServer.scala @@ -0,0 +1,12 @@ +package sttp.tapir.examples.static_content + +import sttp.shared.Identity +import sttp.tapir.emptyInput +import sttp.tapir.files.* +import sttp.tapir.server.netty.sync.NettySyncServer + +@main def staticContentFromFilesNettyServer(): Unit = + NettySyncServer() + .port(8080) + .addEndpoints(staticFilesServerEndpoints[Identity](emptyInput)("/var/www")) + .startAndWait() 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 similarity index 97% rename from examples/src/main/scala/sttp/tapir/examples/static_content/StaticContentFromFilesPekkoServer.scala rename to examples/src/main/scala/sttp/tapir/examples/static_content/staticContentFromFilesPekkoServer.scala index 594a1aa501..e20fe753bb 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 @@ -14,7 +14,7 @@ import java.nio.file.{Files, Path, StandardOpenOption} import scala.concurrent.duration.DurationInt import scala.concurrent.{Await, Future} -object StaticContentFromFilesPekkoServer extends App { +@main def staticContentFromFilesPekkoServer(): Unit = implicit val actorSystem: ActorSystem = ActorSystem() import actorSystem.dispatcher @@ -53,4 +53,3 @@ object StaticContentFromFilesPekkoServer extends App { } 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 similarity index 89% rename from examples/src/main/scala/sttp/tapir/examples/static_content/StaticContentFromResourcesPekkoServer.scala rename to examples/src/main/scala/sttp/tapir/examples/static_content/staticContentFromResourcesPekkoServer.scala index b8f8c2673f..385868090f 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 @@ -11,13 +11,13 @@ import scala.concurrent.duration.DurationInt import scala.concurrent.{Await, Future} import scala.io.StdIn -object StaticContentFromResourcesPekkoServer extends App { +@main def staticContentFromResourcesPekkoServer(): Unit = implicit val actorSystem: ActorSystem = ActorSystem() import actorSystem.dispatcher // we're pretending to be a SPA application, that is we serve index.html if the requested resource cannot be found val resourceEndpoints = staticResourcesGetServerEndpoint[Future](emptyInput)( - StaticContentFromResourcesPekkoServer.getClass.getClassLoader, + this.getClass.getClassLoader, "webapp", FilesOptions.default.defaultFile(List("index.html")) ) @@ -30,4 +30,3 @@ object StaticContentFromResourcesPekkoServer extends App { println("Press any key to exit ...") StdIn.readLine() Await.result(actorSystem.terminate(), 1.minute) -} 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 similarity index 97% rename from examples/src/main/scala/sttp/tapir/examples/static_content/StaticContentSecurePekkoServer.scala rename to examples/src/main/scala/sttp/tapir/examples/static_content/staticContentSecurePekkoServer.scala index d1f3d63520..652f7e7a9b 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 @@ -14,7 +14,7 @@ import java.nio.file.{Files, Path, StandardOpenOption} import scala.concurrent.duration.DurationInt import scala.concurrent.{Await, Future} -object StaticContentSecurePekkoServer extends App { +@main def staticContentSecurePekkoServer(): Unit = // creating test files val exampleDirectory: Path = Files.createTempDirectory("pekko-static-secure-example") Files.write(exampleDirectory.resolve("f1"), "f1 content".getBytes, StandardOpenOption.CREATE_NEW) @@ -58,4 +58,3 @@ object StaticContentSecurePekkoServer extends App { } Await.result(bindAndCheck.flatMap(_.terminate(1.minute)), 1.minute) -} diff --git a/examples/src/main/scala/sttp/tapir/examples/status_code/StatusCodeNettyFutureServer.scala b/examples/src/main/scala/sttp/tapir/examples/status_code/statusCodeNettyServer.scala similarity index 59% rename from examples/src/main/scala/sttp/tapir/examples/status_code/StatusCodeNettyFutureServer.scala rename to examples/src/main/scala/sttp/tapir/examples/status_code/statusCodeNettyServer.scala index 457b4332e2..dfa215d2d0 100644 --- a/examples/src/main/scala/sttp/tapir/examples/status_code/StatusCodeNettyFutureServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/status_code/statusCodeNettyServer.scala @@ -1,22 +1,19 @@ package sttp.tapir.examples.status_code +import ox.supervised import sttp.model.{HeaderNames, StatusCode} import sttp.tapir.* -import sttp.tapir.server.netty.{NettyFutureServer, NettyFutureServerBinding} - -import scala.concurrent.{Await, Future} -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.duration.Duration +import sttp.tapir.server.netty.sync.NettySyncServer /** Three examples of how to return custom status codes */ -object StatusCodeNettyFutureServer extends App { +@main def statusCodeNettyServer(): Unit = // An endpoint which always responds with status code 308 val fixedStatusCodeEndpoint = endpoint.get .in("fixed") .out(statusCode(StatusCode.PermanentRedirect)) .out(header(HeaderNames.Location, "https://adopt-tapir.softwaremill.com")) - .serverLogicPure[Future](_ => Right(())) + .handleSuccess(_ => ()) // @@ -26,7 +23,7 @@ object StatusCodeNettyFutureServer extends App { .errorOut(stringBody) .out(statusCode) .out(stringBody) - .serverLogicPure[Future](code => + .handle(code => StatusCode.safeApply(code) match { // by default, the status code for an error output is 400 case Left(_) => Left(s"Unknown status code: $code") @@ -53,36 +50,28 @@ object StatusCodeNettyFutureServer extends App { oneOfDefaultVariant(emptyOutputAs(Unknown)) // by default, the status code is 400 ) ) - .serverLogicPure[Future](kind => - kind match { - case 1 => Left(NotFound("not found")) // status code 404, as defined in oneOfVariant - case 2 => Left(Unauthorized("secret realm")) // status code 401, as defined in oneOfVariant - case 3 => Right(()) // status code 200 - case _ => Left(Unknown) // status code 400 - } - ) + .handle { + case 1 => Left(NotFound("not found")) // status code 404, as defined in oneOfVariant + case 2 => Left(Unauthorized("secret realm")) // status code 401, as defined in oneOfVariant + case 3 => Right(()) // status code 200 + case _ => Left(Unknown) // status code 400 + } // // Starting netty server val declaredPort = 8080 val declaredHost = "localhost" - val serverBinding: NettyFutureServerBinding = - Await.result( - NettyFutureServer() - .port(declaredPort) - .host(declaredHost) - .addEndpoints(List(fixedStatusCodeEndpoint, dynamicStatusCodeEndpoint, oneOfStatusCodeEndpoint)) - .start(), - Duration.Inf - ) + supervised: + val binding = NettySyncServer() + .port(declaredPort) + .host(declaredHost) + .addEndpoints(List(fixedStatusCodeEndpoint, dynamicStatusCodeEndpoint, oneOfStatusCodeEndpoint)) + .start() - // Bind and start to accept incoming connections. - val port = serverBinding.port - val host = serverBinding.hostName - println(s"Server started at http://$host:$port") - println("Press any key to stop") - System.in.read() + // Bind and start to accept incoming connections. + println(s"Server started at http://${binding.hostName}:${binding.port}") + println("Press any key to stop") + System.in.read() - Await.result(serverBinding.stop(), Duration.Inf) -} + binding.stop() 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 91f38362f2..879649db2a 100644 --- a/examples/src/main/scala/sttp/tapir/examples/streaming/ProxyHttp4sFs2Server.scala +++ b/examples/src/main/scala/sttp/tapir/examples/streaming/ProxyHttp4sFs2Server.scala @@ -1,7 +1,6 @@ package sttp.tapir.examples.streaming import cats.effect.{ExitCode, IO, IOApp} -import sttp.tapir.examples.logging.Logging import fs2.Stream import org.http4s.HttpRoutes import org.http4s.blaze.server.BlazeServerBuilder @@ -14,7 +13,10 @@ import sttp.tapir.* import sttp.tapir.server.http4s.Http4sServerInterpreter /** Proxies requests from /proxy to https://httpbin.org/anything */ -object ProxyHttp4sFs2Server extends IOApp with Logging { +object ProxyHttp4sFs2Server extends IOApp: + import org.slf4j.{Logger, LoggerFactory} + val logger: Logger = LoggerFactory.getLogger(getClass().getName) + val proxyEndpoint: PublicEndpoint[ (Method, List[String], QueryParams, List[Header], Stream[IO, Byte]), Unit, @@ -45,7 +47,7 @@ object ProxyHttp4sFs2Server extends IOApp with Logging { .map { response => (response.headers.toList, response.body) } }) - override def run(args: List[String]): IO[ExitCode] = { + override def run(args: List[String]): IO[ExitCode] = (for { backend <- HttpClientFs2Backend.resource[IO]() routes = proxyRoutes(backend) @@ -56,5 +58,3 @@ object ProxyHttp4sFs2Server extends IOApp with Logging { } yield ()) .use { _ => IO.never } .as(ExitCode.Success) - } -} 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 6914631364..18eadf1a8b 100644 --- a/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingHttp4sFs2Server.scala +++ b/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingHttp4sFs2Server.scala @@ -17,7 +17,7 @@ import java.nio.charset.StandardCharsets import scala.concurrent.duration.* // https://github.com/softwaremill/tapir/issues/367 -object StreamingHttp4sFs2Server extends IOApp { +object StreamingHttp4sFs2Server extends IOApp: // corresponds to: GET /receive?name=... // We need to provide both the schema of the value (for documentation), as well as the format (media type) of the // body. Here, the schema is a `string` (set by `streamTextBody`) and the media type is `text/plain`. @@ -34,7 +34,7 @@ object StreamingHttp4sFs2Server 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] @@ -43,7 +43,7 @@ object StreamingHttp4sFs2Server extends IOApp { .map(s => (size, s)) }) - override def run(args: List[String]): IO[ExitCode] = { + override def run(args: List[String]): IO[ExitCode] = // starting the server BlazeServerBuilder[IO] .bindHttp(8080, "localhost") @@ -51,7 +51,7 @@ object StreamingHttp4sFs2Server extends IOApp { .resource .use { _ => IO { - val backend: SttpBackend[Identity, Any] = HttpURLConnectionBackend() + val backend: SttpBackend[Identity, Any] = HttpClientSyncBackend() val result: String = basicRequest.response(asStringAlways).get(uri"http://localhost:8080/receive").send(backend).body println("Got result: " + result) @@ -59,5 +59,3 @@ object StreamingHttp4sFs2Server extends IOApp { } } .as(ExitCode.Success) - } -} 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 41610fda91..c5fada2682 100644 --- a/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingNettyFs2Server.scala +++ b/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingNettyFs2Server.scala @@ -14,7 +14,7 @@ import sttp.tapir.server.netty.cats.{NettyCatsServer, NettyCatsServerBinding} import java.nio.charset.StandardCharsets import scala.concurrent.duration.* -object StreamingNettyFs2Server extends IOApp { +object StreamingNettyFs2Server extends IOApp: // corresponds to: GET /receive?name=... // We need to provide both the schema of the value (for documentation), as well as the format (media type) of the // body. Here, the schema is a `string` (set by `streamTextBody`) and the media type is `text/plain`. @@ -42,12 +42,11 @@ object StreamingNettyFs2Server extends IOApp { private val declaredPort = 9090 private val declaredHost = "localhost" - override def run(args: List[String]): IO[ExitCode] = { + override def run(args: List[String]): IO[ExitCode] = // starting the server NettyCatsServer .io() .use { server => - val startServer: IO[NettyCatsServerBinding[IO]] = server .port(declaredPort) .host(declaredHost) @@ -61,7 +60,7 @@ object StreamingNettyFs2Server extends IOApp { val host = binding.hostName println(s"Server started at port = ${binding.port}") - val backend: SttpBackend[Identity, Any] = HttpURLConnectionBackend() + val backend: SttpBackend[Identity, Any] = HttpClientSyncBackend() val result: String = basicRequest.response(asStringAlways).get(uri"http://$declaredHost:$declaredPort/receive").send(backend).body println("Got result: " + result) @@ -70,5 +69,3 @@ object StreamingNettyFs2Server extends IOApp { } .as(ExitCode.Success) } - } -} 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 9502fa61e5..469563a166 100644 --- a/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingNettyZioServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingNettyZioServer.scala @@ -13,7 +13,7 @@ import zio.stream.* import java.nio.charset.StandardCharsets -object StreamingNettyZioServer extends ZIOAppDefault { +object StreamingNettyZioServer extends ZIOAppDefault: // corresponds to: GET /receive?name=... // We need to provide both the schema of the value (for documentation), as well as the format (media type) of the // body. Here, the schema is a `string` (set by `streamTextBody`) and the media type is `text/plain`. @@ -38,7 +38,7 @@ object StreamingNettyZioServer extends ZIOAppDefault { private val declaredPort = 9090 private val declaredHost = "localhost" - override def run: URIO[Any, ExitCode] = { + override def run: URIO[Any, ExitCode] = (for { binding <- NettyZioServer() .port(declaredPort) @@ -50,7 +50,7 @@ object StreamingNettyZioServer extends ZIOAppDefault { val host = binding.hostName println(s"Server started at port = ${binding.port}") - val backend: SttpBackend[Identity, Any] = HttpURLConnectionBackend() + val backend: SttpBackend[Identity, Any] = HttpClientSyncBackend() val result: String = basicRequest.response(asStringAlways).get(uri"http://$declaredHost:$declaredPort/receive").send(backend).body println("Got result: " + result) @@ -59,5 +59,3 @@ object StreamingNettyZioServer extends ZIOAppDefault { } _ <- binding.stop() } yield ()).exitCode - } -} 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 40ba9c5cdb..33c2e74676 100644 --- a/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingZioHttpServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingZioHttpServer.scala @@ -12,7 +12,7 @@ import zio.stream.* import java.nio.charset.StandardCharsets import java.time.Duration -object StreamingZioHttpServer extends ZIOAppDefault { +object StreamingZioHttpServer extends ZIOAppDefault: // corresponds to: GET /receive?name=... // We need to provide both the schema of the value (for documentation), as well as the format (media type) of the // body. Here, the schema is a `string` (set by `streamTextBody`) and the media type is `text/plain`. @@ -46,4 +46,3 @@ object StreamingZioHttpServer extends ZIOAppDefault { Server.live ) .exitCode -} diff --git a/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingPekkoServer.scala b/examples/src/main/scala/sttp/tapir/examples/streaming/streamingPekkoServer.scala similarity index 93% rename from examples/src/main/scala/sttp/tapir/examples/streaming/StreamingPekkoServer.scala rename to examples/src/main/scala/sttp/tapir/examples/streaming/streamingPekkoServer.scala index 49e730b91c..9c462ca4a2 100644 --- a/examples/src/main/scala/sttp/tapir/examples/streaming/StreamingPekkoServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/streaming/streamingPekkoServer.scala @@ -14,7 +14,7 @@ import sttp.tapir.* import scala.concurrent.{Await, Future} import scala.concurrent.duration.* -object StreamingPekkoServer extends App { +@main def streamingPekkoServer(): Unit = implicit val actorSystem: ActorSystem = ActorSystem() import actorSystem.dispatcher @@ -31,7 +31,7 @@ object StreamingPekkoServer extends App { // starting the server val bindAndCheck = Http().newServerAt("localhost", 8080).bindFlow(streamingRoute).map { binding => // testing - val backend: SttpBackend[Identity, Any] = HttpURLConnectionBackend() + val backend: SttpBackend[Identity, Any] = HttpClientSyncBackend() val result: String = basicRequest.response(asStringAlways).get(uri"http://localhost:8080/receive").send(backend).body println("Got result: " + result) @@ -41,4 +41,3 @@ object StreamingPekkoServer extends App { } Await.result(bindAndCheck.flatMap(_.terminate(1.minute)), 1.minute) -} 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 b27357cfa9..4d0046f2b1 100644 --- a/examples/src/main/scala/sttp/tapir/examples/testing/PekkoServerStubInterpreterExample.scala +++ b/examples/src/main/scala/sttp/tapir/examples/testing/PekkoServerStubInterpreterExample.scala @@ -15,7 +15,7 @@ import sttp.tapir.server.stub.TapirStubInterpreter import scala.concurrent.{ExecutionContext, Future} -class PekkoServerStubInterpreterExample extends AsyncFlatSpec with Matchers { +class PekkoServerStubInterpreterExample extends AsyncFlatSpec with Matchers: it should "use custom exception handler" in { val stubBackend: SttpBackend[Future, Any] = TapirStubInterpreter(PekkoUsersApi.options, SttpBackendStub.asynchronousFuture) @@ -43,9 +43,8 @@ class PekkoServerStubInterpreterExample extends AsyncFlatSpec with Matchers { // then response.map(_.body shouldBe Right("hello user123")) } -} -object PekkoUsersApi { +object PekkoUsersApi: val greetUser: ServerEndpoint[Any, Future] = endpoint.get .in("api" / "users" / "greet") @@ -64,4 +63,3 @@ object PekkoUsersApi { ) def options(implicit ec: ExecutionContext): CustomiseInterceptors[Future, PekkoHttpServerOptions] = PekkoHttpServerOptions.customiseInterceptors.exceptionHandler(exceptionHandler) -} diff --git a/examples2/src/main/scala/sttp/tapir/examples2/testing/SttpMockServerClientExample.scala b/examples/src/main/scala/sttp/tapir/examples/testing/SttpMockServerClientExample.scala similarity index 95% rename from examples2/src/main/scala/sttp/tapir/examples2/testing/SttpMockServerClientExample.scala rename to examples/src/main/scala/sttp/tapir/examples/testing/SttpMockServerClientExample.scala index 678b641b27..f7faebb25f 100644 --- a/examples2/src/main/scala/sttp/tapir/examples2/testing/SttpMockServerClientExample.scala +++ b/examples/src/main/scala/sttp/tapir/examples/testing/SttpMockServerClientExample.scala @@ -1,4 +1,4 @@ -package sttp.tapir.examples2.testing +package sttp.tapir.examples.testing import io.circe.generic.auto._ import org.mockserver.integration.ClientAndServer.startClientAndServer @@ -15,7 +15,7 @@ import sttp.tapir.server.mockserver._ import scala.util.Success -class SttpMockServerClientExample extends AnyFlatSpec with Matchers with BeforeAndAfterAll with BeforeAndAfterEach { +class SttpMockServerClientExample extends AnyFlatSpec with Matchers with BeforeAndAfterAll with BeforeAndAfterEach: behavior of "SttpMockServerClient" private val baseUri = uri"http://localhost:1080" @@ -59,4 +59,3 @@ class SttpMockServerClientExample extends AnyFlatSpec with Matchers with BeforeA actual shouldEqual Success(Value(Right(sampleOut))) } -} 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 f8b2c770fa..0fa1ba49d6 100644 --- a/examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketHttp4sServer.scala +++ b/examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketHttp4sServer.scala @@ -23,7 +23,7 @@ import sttp.ws.WebSocket import scala.concurrent.ExecutionContext import scala.concurrent.duration.* -object WebSocketHttp4sServer extends IOApp { +object WebSocketHttp4sServer extends IOApp: implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global @@ -69,7 +69,7 @@ object WebSocketHttp4sServer extends IOApp { val apiDocs = AsyncAPIInterpreter().toAsyncAPI(wsEndpoint, "Byte counter", "1.0", List("dev" -> Server("localhost:8080", "ws"))).toYaml println(s"Paste into https://playground.asyncapi.io/ to see the docs for this endpoint:\n$apiDocs") - override def run(args: List[String]): IO[ExitCode] = { + override def run(args: List[String]): IO[ExitCode] = // Starting the server BlazeServerBuilder[IO] .withExecutionContext(ec) @@ -107,5 +107,3 @@ object WebSocketHttp4sServer extends IOApp { .map(_ => println("Counting complete, bye!")) } .as(ExitCode.Success) - } -} diff --git a/examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketPekkoServer.scala b/examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketPekkoServer.scala deleted file mode 100644 index 3837fcb3e3..0000000000 --- a/examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketPekkoServer.scala +++ /dev/null @@ -1,76 +0,0 @@ -package sttp.tapir.examples.websocket - -import io.circe.generic.auto.* -import org.apache.pekko.actor.ActorSystem -import org.apache.pekko.http.scaladsl.Http -import org.apache.pekko.http.scaladsl.server.Route -import org.apache.pekko.stream.scaladsl.Flow -import sttp.apispec.asyncapi.Server -import sttp.apispec.asyncapi.circe.yaml.* -import sttp.capabilities.WebSockets -import sttp.capabilities.pekko.PekkoStreams -import sttp.client3.* -import sttp.client3.pekkohttp.PekkoHttpBackend -import sttp.tapir.* -import sttp.tapir.docs.asyncapi.AsyncAPIInterpreter -import sttp.tapir.generic.auto.* -import sttp.tapir.json.circe.* -import sttp.tapir.server.pekkohttp.PekkoHttpServerInterpreter -import sttp.ws.WebSocket - -import scala.concurrent.duration.* -import scala.concurrent.{Await, Future} - -object WebSocketPekkoServer { - def main(args: Array[String]): Unit = { - case class Response(hello: String) - - // 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] = - endpoint.get.in("ping").out(webSocketBody[String, CodecFormat.TextPlain, Response, CodecFormat.Json](PekkoStreams)) - - implicit val actorSystem: ActorSystem = ActorSystem() - import actorSystem.dispatcher - - // Implementation of the web socket: a flow which echoes incoming messages - val wsRoute: Route = - PekkoHttpServerInterpreter().toRoute( - wsEndpoint.serverLogicSuccess(_ => Future.successful(Flow.fromFunction((in: String) => Response(in)): Flow[String, Response, Any])) - ) - - // Documentation - val apiDocs = AsyncAPIInterpreter().toAsyncAPI(wsEndpoint, "JSON echo", "1.0", List("dev" -> Server("localhost:8080", "ws"))).toYaml - println(s"Paste into https://playground.asyncapi.io/ to see the docs for this endpoint:\n$apiDocs") - - // Starting the server - val bindAndCheck = Http() - .newServerAt("localhost", 8080) - .bindFlow(wsRoute) - .flatMap { binding => - // We could have interpreted wsEndpoint as a client, but here we are using sttp client directly - val backend: SttpBackend[Future, WebSockets] = PekkoHttpBackend.usingActorSystem(actorSystem) - // Client which interacts with the web socket - basicRequest - .response(asWebSocket { (ws: WebSocket[Future]) => - for { - _ <- ws.sendText("world") - _ <- ws.sendText("there") - r1 <- ws.receiveText() - _ = println(r1) - r2 <- ws.receiveText() - _ = println(r2) - _ <- ws.sendText("how are you") - r3 <- ws.receiveText() - _ = println(r3) - } yield () - }) - .get(uri"ws://localhost:8080/ping") - .send(backend) - .map(_ => binding) - } - - Await.result(bindAndCheck.flatMap(_.terminate(1.minute)).flatMap(_ => actorSystem.terminate()), 1.minute) - } -} diff --git a/examples/src/main/scala/sttp/tapir/examples/websocket/webSocketPekkoServer.scala b/examples/src/main/scala/sttp/tapir/examples/websocket/webSocketPekkoServer.scala new file mode 100644 index 0000000000..7363896264 --- /dev/null +++ b/examples/src/main/scala/sttp/tapir/examples/websocket/webSocketPekkoServer.scala @@ -0,0 +1,73 @@ +package sttp.tapir.examples.websocket + +import io.circe.generic.auto.* +import org.apache.pekko.actor.ActorSystem +import org.apache.pekko.http.scaladsl.Http +import org.apache.pekko.http.scaladsl.server.Route +import org.apache.pekko.stream.scaladsl.Flow +import sttp.apispec.asyncapi.Server +import sttp.apispec.asyncapi.circe.yaml.* +import sttp.capabilities.WebSockets +import sttp.capabilities.pekko.PekkoStreams +import sttp.client3.* +import sttp.client3.pekkohttp.PekkoHttpBackend +import sttp.tapir.* +import sttp.tapir.docs.asyncapi.AsyncAPIInterpreter +import sttp.tapir.generic.auto.* +import sttp.tapir.json.circe.* +import sttp.tapir.server.pekkohttp.PekkoHttpServerInterpreter +import sttp.ws.WebSocket + +import scala.concurrent.duration.* +import scala.concurrent.{Await, Future} + +@main def webSocketPekkoServer(): Unit = + case class Response(hello: String) + + // 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] = + endpoint.get.in("ping").out(webSocketBody[String, CodecFormat.TextPlain, Response, CodecFormat.Json](PekkoStreams)) + + implicit val actorSystem: ActorSystem = ActorSystem() + import actorSystem.dispatcher + + // Implementation of the web socket: a flow which echoes incoming messages + val wsRoute: Route = + PekkoHttpServerInterpreter().toRoute( + wsEndpoint.serverLogicSuccess(_ => Future.successful(Flow.fromFunction((in: String) => Response(in)): Flow[String, Response, Any])) + ) + + // Documentation + val apiDocs = AsyncAPIInterpreter().toAsyncAPI(wsEndpoint, "JSON echo", "1.0", List("dev" -> Server("localhost:8080", "ws"))).toYaml + println(s"Paste into https://playground.asyncapi.io/ to see the docs for this endpoint:\n$apiDocs") + + // Starting the server + val bindAndCheck = Http() + .newServerAt("localhost", 8080) + .bindFlow(wsRoute) + .flatMap { binding => + // We could have interpreted wsEndpoint as a client, but here we are using sttp client directly + val backend: SttpBackend[Future, WebSockets] = PekkoHttpBackend.usingActorSystem(actorSystem) + // Client which interacts with the web socket + basicRequest + .response(asWebSocket { (ws: WebSocket[Future]) => + for { + _ <- ws.sendText("world") + _ <- ws.sendText("there") + r1 <- ws.receiveText() + _ = println(r1) + r2 <- ws.receiveText() + _ = println(r2) + _ <- ws.sendText("how are you") + r3 <- ws.receiveText() + _ = println(r3) + } yield () + }) + .get(uri"ws://localhost:8080/ping") + .send(backend) + .map(_ => binding) + } + + Await.result(bindAndCheck.flatMap(_.terminate(1.minute)).flatMap(_ => actorSystem.terminate()), 1.minute) diff --git a/examples2/src/main/resources/logback.xml b/examples2/src/main/resources/logback.xml deleted file mode 100644 index e6cee15ae7..0000000000 --- a/examples2/src/main/resources/logback.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - %date [%thread] %-5level %logger{36} - %msg%n - - - - - - - \ No newline at end of file diff --git a/examples2/src/main/scala/sttp/tapir/examples2/HelloWorldAkkaServer.scala b/examples2/src/main/scala/sttp/tapir/examples2/HelloWorldAkkaServer.scala deleted file mode 100644 index be26b6a47e..0000000000 --- a/examples2/src/main/scala/sttp/tapir/examples2/HelloWorldAkkaServer.scala +++ /dev/null @@ -1,38 +0,0 @@ -package sttp.tapir.examples2 - -import akka.actor.ActorSystem -import akka.http.scaladsl.Http -import akka.http.scaladsl.server.Route -import sttp.shared.Identity -import sttp.tapir._ -import sttp.tapir.server.akkahttp.AkkaHttpServerInterpreter - -import scala.concurrent.{Await, Future} -import scala.concurrent.duration._ -import sttp.client3._ - -object HelloWorldAkkaServer extends App { - implicit val actorSystem: ActorSystem = ActorSystem() - import actorSystem.dispatcher - - // the endpoint: single fixed path input ("hello"), single query parameter - // corresponds to: GET /hello?name=... - val helloWorld: PublicEndpoint[String, Unit, String, Any] = - endpoint.get.in("hello").in(query[String]("name")).out(stringBody) - - // converting an endpoint to a route (providing server-side logic); extension method comes from imported packages - val helloWorldRoute: Route = - AkkaHttpServerInterpreter().toRoute(helloWorld.serverLogicSuccess(name => Future.successful(s"Hello, $name!"))) - - // starting the server - val bindAndCheck = Http().newServerAt("localhost", 8080).bindFlow(helloWorldRoute).map { _ => - // testing - val backend: SttpBackend[Identity, Any] = HttpURLConnectionBackend() - val result: String = basicRequest.response(asStringAlways).get(uri"http://localhost:8080/hello?name=Frodo").send(backend).body - println("Got result: " + result) - - assert(result == "Hello, Frodo!") - } - - Await.result(bindAndCheck.transformWith { r => actorSystem.terminate().transform(_ => r) }, 1.minute) -} diff --git a/examples2/src/main/scala/sttp/tapir/examples2/custom_types/SealedTraitWithDiscriminator.scala b/examples2/src/main/scala/sttp/tapir/examples2/custom_types/SealedTraitWithDiscriminator.scala deleted file mode 100644 index 075452d6af..0000000000 --- a/examples2/src/main/scala/sttp/tapir/examples2/custom_types/SealedTraitWithDiscriminator.scala +++ /dev/null @@ -1,53 +0,0 @@ -package sttp.tapir.examples2.custom_types - -// Note that you'll need the extras.auto._ import, not the usual one -import io.circe.generic.extras.auto._ -import io.circe.generic.extras.{Configuration => CirceConfiguration} -import sttp.tapir._ -import sttp.tapir.generic.Configuration -import sttp.tapir.generic.auto._ -import sttp.tapir.json.circe._ -import sttp.tapir.server.ServerEndpoint -import sttp.tapir.server.netty.{NettyFutureServer, NettyFutureServerBinding} -import sttp.tapir.swagger.bundle.SwaggerInterpreter - -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.duration.Duration -import scala.concurrent.{Await, Future} - -object SealedTraitWithDiscriminator extends App { - // data structures - sealed trait Node - case class Leaf(value: String) extends Node - case class Branch(name: String, children: Seq[Node]) extends Node - - val discriminatorFieldName = "kind" - - // these configs must match: one configures tapir's schema, the other circe's encoding/decoding to/from json - implicit val tapirConfig: Configuration = Configuration.default.withDiscriminator(discriminatorFieldName) - // the CirceConfiguration class is a renamed import (see above), as the name would clash with tapir's Configuration - implicit val circeConfig: CirceConfiguration = CirceConfiguration.default.withDiscriminator(discriminatorFieldName) - - // endpoint description; the configs are used when deriving the Schema, Encoder and Decoder, which are implicit - // parameters to jsonBody[Node] - val nodesListing: PublicEndpoint[Unit, Unit, Node, Any] = endpoint.get - .in("nodes") - .out(jsonBody[Node]) - - val nodesListingServerEndpoint: ServerEndpoint[Any, Future] = - nodesListing.serverLogicSuccess[Future](_ => Future.successful(Branch("b", List(Leaf("x"), Leaf("y"))))) - - val docsEndpoints = SwaggerInterpreter().fromServerEndpoints[Future](List(nodesListingServerEndpoint), "Nodes", "1.0.0") - - val serverBinding: NettyFutureServerBinding = - Await.result( - NettyFutureServer().addEndpoints(nodesListingServerEndpoint :: docsEndpoints).start(), - Duration.Inf - ) - - println("Go to: http://localhost:8080/docs") - println("Press any key to exit ...") - scala.io.StdIn.readLine() - - Await.result(serverBinding.stop(), Duration.Inf) -} diff --git a/examples2/src/main/scala/sttp/tapir/examples2/websocket/WebSocketAkkaClient.scala b/examples2/src/main/scala/sttp/tapir/examples2/websocket/WebSocketAkkaClient.scala deleted file mode 100644 index 755c6cda12..0000000000 --- a/examples2/src/main/scala/sttp/tapir/examples2/websocket/WebSocketAkkaClient.scala +++ /dev/null @@ -1,52 +0,0 @@ -package sttp.tapir.examples2.websocket - -import akka.actor.ActorSystem -import akka.stream.scaladsl.{Flow, Sink, Source} -import io.circe.generic.auto._ -import sttp.capabilities.WebSockets -import sttp.capabilities.akka.AkkaStreams -import sttp.client3._ -import sttp.client3.akkahttp.AkkaHttpBackend -import sttp.tapir._ -import sttp.tapir.client.sttp.SttpClientInterpreter -import sttp.tapir.json.circe._ -import sttp.tapir.generic.auto._ -import sttp.tapir.client.sttp.ws.akkahttp._ - -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.duration._ -import scala.concurrent.{Await, Future} -object WebSocketAkkaClient extends App { - case class TestMessage(text: String, counter: Int) - - val jsonEchoWsEndpoint: PublicEndpoint[Unit, Unit, Flow[TestMessage, TestMessage, Any], AkkaStreams with WebSockets] = - endpoint.get.out(webSocketBody[TestMessage, CodecFormat.Json, TestMessage, CodecFormat.Json](AkkaStreams)) - - implicit val actorSystem: ActorSystem = ActorSystem() - val backend = AkkaHttpBackend.usingActorSystem(actorSystem) - val result = SttpClientInterpreter() - .toClientThrowDecodeFailures(jsonEchoWsEndpoint, Some(uri"wss://echo.websocket.org"), backend) - .apply(()) - .flatMap { - case Left(msg) => Future(println(s"Cannot establish web socket: $msg")) - case Right(serverFlow) => - val queue = serverFlow - .runWith( - Source(List(TestMessage("msg1", 10), TestMessage("msg2", 20), TestMessage("msg3", 30))), - Sink.queue() - ) - ._2 - - for { - response1 <- queue.pull() - _ = println(s"Response 1: $response1") - response2 <- queue.pull() - _ = println(s"Response 2: $response2") - response3 <- queue.pull() - _ = println(s"Response 3: $response3") - } yield queue.cancel() - } - .flatMap(_ => actorSystem.terminate()) - - Await.result(result, 1.minute) -} diff --git a/json/json4s/src/test/scalajvm/sttp/tapir/json/json4s/TapirJson4sTests.scala b/json/json4s/src/test/scala-2/sttp/tapir/json/json4s/TapirJson4sTests.scala similarity index 94% rename from json/json4s/src/test/scalajvm/sttp/tapir/json/json4s/TapirJson4sTests.scala rename to json/json4s/src/test/scala-2/sttp/tapir/json/json4s/TapirJson4sTests.scala index 6617b59ecf..a0870c2baf 100644 --- a/json/json4s/src/test/scalajvm/sttp/tapir/json/json4s/TapirJson4sTests.scala +++ b/json/json4s/src/test/scala-2/sttp/tapir/json/json4s/TapirJson4sTests.scala @@ -14,6 +14,8 @@ case class Customer(name: String, yearOfBirth: Int, lastPurchase: Option[Long]) case class Item(serialNumber: Long, price: Int) case class Order(items: Seq[Item], customer: Customer) +// the tests are run only for scala 2 because https://github.com/json4s/json4s/issues/1035 is only fixed in +// versions for scala 3.4+ (not LTS) class TapirJson4sTests extends AnyFlatSpecLike with Matchers { implicit val serialization: Serialization = org.json4s.jackson.Serialization diff --git a/server/netty-server/sync/src/main/scala/sttp/tapir/server/netty/sync/NettySyncServerInterpreter.scala b/server/netty-server/sync/src/main/scala/sttp/tapir/server/netty/sync/NettySyncServerInterpreter.scala index 0beab5e08d..8e98694f08 100644 --- a/server/netty-server/sync/src/main/scala/sttp/tapir/server/netty/sync/NettySyncServerInterpreter.scala +++ b/server/netty-server/sync/src/main/scala/sttp/tapir/server/netty/sync/NettySyncServerInterpreter.scala @@ -15,8 +15,7 @@ import sttp.tapir.server.netty.{NettyResponse, NettyServerRequest, Route} trait NettySyncServerInterpreter: def nettyServerOptions: NettySyncServerOptions - /** Requires implicit supervision scope (Ox), because it needs to know in which scope it can start background forks in the Web Sockets - * processor. + /** Requires supervision scope (Ox), because it needs to know in which scope it can start background forks in the Web Sockets processor. */ def toRoute( ses: List[ServerEndpoint[OxStreams & WebSockets, Identity]], From 951d7360de9c3f4c02207bb5c019055e0fa6845a Mon Sep 17 00:00:00 2001 From: softwaremill-ci Date: Sat, 6 Jul 2024 02:36:25 +0200 Subject: [PATCH 2/8] Update jackson-module-scala to 2.17.2 (#3905) --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index b10cd7c8ce..1681c33537 100644 --- a/build.sbt +++ b/build.sbt @@ -523,7 +523,7 @@ lazy val perfTests: ProjectMatrix = (projectMatrix in file("perf-tests")) "jackson-databind" ), "io.gatling" % "gatling-test-framework" % "3.11.5" % "test" exclude ("com.fasterxml.jackson.core", "jackson-databind"), - "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.17.1", + "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.17.2", "nl.grons" %% "metrics4-scala" % Versions.metrics4Scala % Test, "com.lihaoyi" %% "scalatags" % Versions.scalaTags % Test, "io.github.classgraph" % "classgraph" % "4.8.174", From 3f1f124916923f3df6a79130836a4d939b1206cd Mon Sep 17 00:00:00 2001 From: softwaremill-ci Date: Mon, 8 Jul 2024 15:33:19 +0200 Subject: [PATCH 3/8] Update jsoniter-scala-core, ... to 2.30.4 (#3911) Co-authored-by: scala-steward --- build.sbt | 4 ++-- .../oneOf-json-roundtrip_jsoniter/build.sbt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.sbt b/build.sbt index 1681c33537..1a8d2f1c65 100644 --- a/build.sbt +++ b/build.sbt @@ -920,8 +920,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.3", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.30.3" % Test, + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.30.4", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.30.4" % Test, scalaTest.value % Test ) ) 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 6d35e824b5..1377856037 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.3", - "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % "2.30.3", - "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % "2.30.3" % "compile-internal", + "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % "2.30.4", + "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % "2.30.4" % "compile-internal", "org.scalatest" %% "scalatest" % "3.2.19" % Test, "com.softwaremill.sttp.tapir" %% "tapir-sttp-stub-server" % "1.10.0" % Test ) From 424e2d2b279ff26ded9314d8ff1df031cd0e00d3 Mon Sep 17 00:00:00 2001 From: Adam Warski Date: Mon, 8 Jul 2024 15:43:17 +0200 Subject: [PATCH 4/8] Fix extractFromRequest in zio http endpoints (#3913) --- .../server/ziohttp/ZioHttpInterpreter.scala | 25 ++++++++----------- .../server/ziohttp/ZioHttpServerTest.scala | 18 +++++++++++++ 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/server/zio-http-server/src/main/scala/sttp/tapir/server/ziohttp/ZioHttpInterpreter.scala b/server/zio-http-server/src/main/scala/sttp/tapir/server/ziohttp/ZioHttpInterpreter.scala index 2561b0a3bd..a8d1789b7f 100644 --- a/server/zio-http-server/src/main/scala/sttp/tapir/server/ziohttp/ZioHttpInterpreter.scala +++ b/server/zio-http-server/src/main/scala/sttp/tapir/server/ziohttp/ZioHttpInterpreter.scala @@ -87,30 +87,25 @@ trait ZioHttpInterpreter[R] { case _ => false } - val pattern = if (hasPath) { + val pathPrefixPattern = if (hasPath) { val initialPattern = RoutePattern(Method.ANY, PathCodec.empty).asInstanceOf[RoutePattern[Any]] - // The second tuple parameter specifies if PathCodec.trailing has already been added to the route's pattern - - // it can only be added once. It's possible that an endpoint contains both ExtractFromRequest & PathsCapture, - // which would cause it to be added twice. - inputs - .foldLeft((initialPattern, false)) { case ((p, trailingAdded), component) => + val base = inputs + .foldLeft(initialPattern) { case (p, component) => component match { - case i: EndpointInput.PathCapture[_] => - ((p / PathCodec.string(i.name.getOrElse("?"))).asInstanceOf[RoutePattern[Any]], trailingAdded) - case _: EndpointInput.ExtractFromRequest[_] if !trailingAdded => - ((p / PathCodec.trailing).asInstanceOf[RoutePattern[Any]], true) - case _: EndpointInput.PathsCapture[_] if !trailingAdded => ((p / PathCodec.trailing).asInstanceOf[RoutePattern[Any]], true) - case i: EndpointInput.FixedPath[_] => (p / PathCodec.literal(i.s), trailingAdded) - case _ => (p, trailingAdded) + case i: EndpointInput.PathCapture[_] => (p / PathCodec.string(i.name.getOrElse("?"))).asInstanceOf[RoutePattern[Any]] + case i: EndpointInput.FixedPath[_] => p / PathCodec.literal(i.s) + case _ => p } } - ._1 + // because we capture the path prefix, we add a matcher for arbitrary other path components (which might be + // handled by tapir's `paths` or `extractFromRequest`) + base / PathCodec.trailing } else { // if there are no path inputs, we return a catch-all RoutePattern(Method.ANY, PathCodec.trailing).asInstanceOf[RoutePattern[Any]] } - Route.handled(pattern)(Handler.fromFunctionHandler { (request: Request) => handleRequest(request, sesForPathTemplate) }) + Route.handled(pathPrefixPattern)(Handler.fromFunctionHandler { (request: Request) => handleRequest(request, sesForPathTemplate) }) } Routes(Chunk.fromIterable(handlers)) diff --git a/server/zio-http-server/src/test/scala/sttp/tapir/server/ziohttp/ZioHttpServerTest.scala b/server/zio-http-server/src/test/scala/sttp/tapir/server/ziohttp/ZioHttpServerTest.scala index e7789272b4..9e266d72d8 100644 --- a/server/zio-http-server/src/test/scala/sttp/tapir/server/ziohttp/ZioHttpServerTest.scala +++ b/server/zio-http-server/src/test/scala/sttp/tapir/server/ziohttp/ZioHttpServerTest.scala @@ -163,6 +163,24 @@ class ZioHttpServerTest extends TestSuite { Unsafe.unsafe(implicit u => r.unsafe.runToFuture(test)) }, + // https://github.com/softwaremill/tapir/issues/3907 + Test("extractFromRequest in the middle") { + val ep = endpoint.get + .in(path[String]) + .in(extractFromRequest(_.method)) + .in("test" / path[String]) + .out(stringBody) + .zServerLogic[Any](_ => ZIO.succeed("works")) + val route = ZioHttpInterpreter().toHttp(ep) + + val test: UIO[Assertion] = route + .runZIO(Request.get(url = URL(Path.empty / "p1" / "test" / "p2"))) + .flatMap(response => response.body.asString) + .map(_ shouldBe "works") + .catchAll(_ => ZIO.succeed[Assertion](fail("Unable to extract body from Http response"))) + + Unsafe.unsafe(implicit u => r.unsafe.runToFuture(test)) + }, // https://github.com/softwaremill/tapir/issues/2849 Test("Streaming works through the stub backend") { // given From 71619f5f4a6d7a1545141aec900fd07b85e0f2cb Mon Sep 17 00:00:00 2001 From: softwaremill-ci Date: Tue, 9 Jul 2024 08:10:46 +0200 Subject: [PATCH 5/8] Update sbt, sbt-dependency-tree to 1.10.1 (#3912) Co-authored-by: scala-steward --- .../sbt-openapi-codegen/caching/project/build.properties | 2 +- .../sbt-openapi-codegen/minimal/project/build.properties | 2 +- .../oneOf-json-roundtrip/project/build.properties | 2 +- .../oneOf-json-roundtrip_jsoniter/project/build.properties | 2 +- .../oneOf-json-roundtrip_scala3/project/build.properties | 2 +- .../option-overrides/project/build.properties | 2 +- project/build.properties | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/caching/project/build.properties b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/caching/project/build.properties index 081fdbbc76..ee4c672cd0 100644 --- a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/caching/project/build.properties +++ b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/caching/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.0 +sbt.version=1.10.1 diff --git a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/minimal/project/build.properties b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/minimal/project/build.properties index 081fdbbc76..ee4c672cd0 100644 --- a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/minimal/project/build.properties +++ b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/minimal/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.0 +sbt.version=1.10.1 diff --git a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip/project/build.properties b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip/project/build.properties index 081fdbbc76..ee4c672cd0 100644 --- a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip/project/build.properties +++ b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.0 +sbt.version=1.10.1 diff --git a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter/project/build.properties b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter/project/build.properties index 081fdbbc76..ee4c672cd0 100644 --- a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter/project/build.properties +++ b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.0 +sbt.version=1.10.1 diff --git a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_scala3/project/build.properties b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_scala3/project/build.properties index 081fdbbc76..ee4c672cd0 100644 --- a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_scala3/project/build.properties +++ b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_scala3/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.0 +sbt.version=1.10.1 diff --git a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/option-overrides/project/build.properties b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/option-overrides/project/build.properties index 081fdbbc76..ee4c672cd0 100644 --- a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/option-overrides/project/build.properties +++ b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/option-overrides/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.0 +sbt.version=1.10.1 diff --git a/project/build.properties b/project/build.properties index 081fdbbc76..ee4c672cd0 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.0 +sbt.version=1.10.1 From 8cdd0bfd98f02511c47a8cc2d216f3d0ebd8c9c2 Mon Sep 17 00:00:00 2001 From: softwaremill-ci Date: Tue, 9 Jul 2024 09:10:27 +0200 Subject: [PATCH 6/8] Update enumeratum, enumeratum-circe to 1.7.4 (#3910) Co-authored-by: scala-steward Co-authored-by: Krzysztof Ciesielski --- build.sbt | 4 ++-- .../sbt-openapi-codegen/oneOf-json-roundtrip/build.sbt | 4 ++-- .../oneOf-json-roundtrip_jsoniter/build.sbt | 2 +- project/Versions.scala | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/build.sbt b/build.sbt index 1a8d2f1c65..07aaa1cb6a 100644 --- a/build.sbt +++ b/build.sbt @@ -1976,8 +1976,8 @@ lazy val openapiCodegenCore: ProjectMatrix = (projectMatrix in file("openapi-cod "com.47deg" %% "scalacheck-toolbox-datetime" % "0.7.0" % Test, scalaOrganization.value % "scala-reflect" % scalaVersion.value, scalaOrganization.value % "scala-compiler" % scalaVersion.value % Test, - "com.beachape" %% "enumeratum" % "1.7.3" % Test, - "com.beachape" %% "enumeratum-circe" % "1.7.3" % Test, + "com.beachape" %% "enumeratum" % "1.7.4" % Test, + "com.beachape" %% "enumeratum-circe" % "1.7.4" % Test, "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % "2.28.2" % Test, "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % "2.28.2" % Provided ) diff --git a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip/build.sbt b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip/build.sbt index b95400696b..5d3fceb614 100644 --- a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip/build.sbt +++ b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip/build.sbt @@ -10,8 +10,8 @@ libraryDependencies ++= Seq( "com.softwaremill.sttp.tapir" %% "tapir-openapi-docs" % "1.10.0", "com.softwaremill.sttp.apispec" %% "openapi-circe-yaml" % "0.8.0", "io.circe" %% "circe-generic" % "0.14.9", - "com.beachape" %% "enumeratum" % "1.7.3", - "com.beachape" %% "enumeratum-circe" % "1.7.3", + "com.beachape" %% "enumeratum" % "1.7.4", + "com.beachape" %% "enumeratum-circe" % "1.7.4", "org.scalatest" %% "scalatest" % "3.2.19" % Test, "com.softwaremill.sttp.tapir" %% "tapir-sttp-stub-server" % "1.10.0" % Test ) diff --git a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter/build.sbt b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter/build.sbt index 1377856037..f86e7bbb23 100644 --- a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter/build.sbt +++ b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter/build.sbt @@ -10,7 +10,7 @@ libraryDependencies ++= Seq( "com.softwaremill.sttp.tapir" %% "tapir-jsoniter-scala" % "1.10.0", "com.softwaremill.sttp.tapir" %% "tapir-openapi-docs" % "1.10.0", "com.softwaremill.sttp.apispec" %% "openapi-circe-yaml" % "0.8.0", - "com.beachape" %% "enumeratum" % "1.7.3", + "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", "org.scalatest" %% "scalatest" % "3.2.19" % Test, diff --git a/project/Versions.scala b/project/Versions.scala index 1eba13926c..4ef9a5a40e 100644 --- a/project/Versions.scala +++ b/project/Versions.scala @@ -34,7 +34,7 @@ object Versions { val scalaTestPlusScalaCheck = "3.2.19.0" val refined = "0.11.2" val iron = "2.6.0" - val enumeratum = "1.7.3" + val enumeratum = "1.7.4" val zio = "2.1.5" val zioHttp = "3.0.0-RC8" val zioInteropCats = "23.0.0.8" From 1c15e11d9dd25343315384d94f20ce35a8f2f476 Mon Sep 17 00:00:00 2001 From: softwaremill-ci Date: Tue, 9 Jul 2024 09:54:51 +0200 Subject: [PATCH 7/8] Update opentelemetry-api, ... to 1.40.0 (#3906) --- project/Versions.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Versions.scala b/project/Versions.scala index 4ef9a5a40e..d1a2419a20 100644 --- a/project/Versions.scala +++ b/project/Versions.scala @@ -60,7 +60,7 @@ object Versions { val fs2 = "3.10.2" val decline = "2.4.1" val quicklens = "1.9.7" - val openTelemetry = "1.39.0" + val openTelemetry = "1.40.0" val mockServer = "5.15.0" val dogstatsdClient = "4.4.2" val nettyAll = "4.1.111.Final" From eb409218fa914145fdf5aa95d66d9c3705f6bc9a Mon Sep 17 00:00:00 2001 From: softwaremill-ci Date: Wed, 10 Jul 2024 02:38:50 +0200 Subject: [PATCH 8/8] Update sbt-mdoc to 2.5.4 (#3920) --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 83926fc598..bf1132286d 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -8,7 +8,7 @@ addSbtPlugin("com.softwaremill.sbt-softwaremill" % "sbt-softwaremill-browser-tes //addSbtPlugin("io.spray" % "sbt-boilerplate" % "0.6.1") addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.3") addSbtPlugin("org.playframework.twirl" % "sbt-twirl" % "2.0.7") -addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.5.3") +addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.5.4") addSbtPlugin("com.eed3si9n" % "sbt-projectmatrix" % "0.10.0") addSbtPlugin("org.jetbrains.scala" % "sbt-ide-settings" % "1.1.2") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0")