Skip to content

Commit 54e990f

Browse files
committed
Merge remote-tracking branch 'tapir/master' into http4s-migrate-to-ember
2 parents ba6aa05 + 9ee00fe commit 54e990f

File tree

36 files changed

+330
-50
lines changed

36 files changed

+330
-50
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,4 +251,4 @@ We offer commercial support for tapir and related technologies, as well as devel
251251

252252
## Copyright
253253

254-
Copyright (C) 2018-2024 SoftwareMill [https://softwaremill.com](https://softwaremill.com).
254+
Copyright (C) 2018-2025 SoftwareMill [https://softwaremill.com](https://softwaremill.com).

build.sbt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1000,7 +1000,7 @@ lazy val pekkoGrpcExamples: ProjectMatrix = (projectMatrix in file("grpc/pekko-e
10001000
.settings(
10011001
name := "tapir-pekko-grpc-examples",
10021002
libraryDependencies ++= Seq(
1003-
"org.apache.pekko" %% "pekko-discovery" % "1.1.2",
1003+
"org.apache.pekko" %% "pekko-discovery" % "1.1.3",
10041004
slf4j
10051005
),
10061006
fork := true
@@ -2077,7 +2077,9 @@ lazy val examples: ProjectMatrix = (projectMatrix in file("examples"))
20772077
"io.opentelemetry" % "opentelemetry-sdk" % Versions.openTelemetry,
20782078
"io.opentelemetry" % "opentelemetry-sdk-metrics" % Versions.openTelemetry,
20792079
"io.opentelemetry" % "opentelemetry-exporter-otlp" % Versions.openTelemetry,
2080+
"io.opentelemetry" % "opentelemetry-sdk-extension-autoconfigure" % Versions.openTelemetry,
20802081
"com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % Versions.jsoniter,
2082+
"org.typelevel" %% "otel4s-oteljava" % Versions.otel4s,
20812083
scalaTest.value,
20822084
logback
20832085
),
@@ -2110,6 +2112,7 @@ lazy val examples: ProjectMatrix = (projectMatrix in file("examples"))
21102112
sttpStubServer,
21112113
swaggerUiBundle,
21122114
redocBundle,
2115+
vertxServer,
21132116
zioHttpServer,
21142117
zioJson,
21152118
zioMetrics

doc/endpoint/websockets.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,22 @@ When creating a `webSocketBody`, we need to provide the following parameters:
3434
By default, ping-pong frames are handled automatically, fragmented frames are combined, and close frames aren't
3535
decoded, but this can be customised through methods on `webSocketBody`.
3636

37+
## Close frames
38+
39+
If you are using the default codecs between `WebSocketFrame` and your high-level types, and you'd like to either be
40+
notified that a websocket has been closed by the client, or close it from the server side, then you should wrap your
41+
high-level type into an `Option`.
42+
43+
The default codecs map close frames to `None`, and regular (decoded text/binary) frames to `Some`. Hence, using the
44+
following definition:
45+
46+
```scala
47+
webSocketBody[Option[String], CodecFormat.TextPlain, Option[Response], CodecFormat.Json](PekkoStreams)
48+
```
49+
50+
the websocket-processing pipe will receive a `None: Option[String]` when the client closes the web socket. Moreover,
51+
if the pipe emits a `None: Option[Response]`, the web socket will be closed by the server.
52+
3753
## Raw web sockets
3854

3955
Alternatively, it's possible to obtain a raw pipe transforming `WebSocketFrame`s:

doc/server/zio-http4s.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,4 +174,4 @@ val routes: HttpRoutes[Task] =
174174

175175
## Examples
176176

177-
There's a couple of [examples](../examples.md) of using the ZIO integration available.\
177+
There's a couple of [examples](../examples.md) of using the ZIO integration available.

doc/support.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Sponsors
44

5-
Development and maintenance of sttp tapir is sponsored by [SoftwareMill](https://softwaremill.com), a software development and consulting company. We help clients scale their business through software. We offer services around migrating and maintaining Java and Scala projects (e.g. to Java 21, or across Scala versions), ML/AI discovery workshops, introducing developer platforms (based on Kubernetes and observability technologies), and others. Our areas of expertise include performant backends, distributed systems, machine learning and data analytics, with a focus on Java, Scala, Kafka, TypeScript and Rust.
5+
Development and maintenance of Tapir is sponsored by [SoftwareMill](https://softwaremill.com), a software development and consulting company. We help clients scale their business through software. We offer services around migrating and maintaining Java and Scala projects (e.g. to Java 21, or across Scala versions), ML/AI discovery workshops, introducing developer platforms (based on Kubernetes and observability technologies), and others. Our areas of expertise include performant backends, distributed systems, machine learning and data analytics, with a focus on Java, Scala, Kafka, TypeScript and Rust.
66

77
[![](https://files.softwaremill.com/logo/logo.png "SoftwareMill")](https://softwaremill.com)
88

examples/src/main/scala/sttp/tapir/examples/HelloWorldHttp4sServer.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.11
44
//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.11
55
//> using dep com.softwaremill.sttp.client3::core:3.9.8
6-
//> using dep org.http4s::http4s-ember-server:0.23.16
6+
//> using dep org.http4s::http4s-ember-server:0.23.30
77

88
package sttp.tapir.examples
99

examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server-zio:1.11.11
66
//> using dep com.softwaremill.sttp.tapir::tapir-zio:1.11.11
77
//> using dep org.http4s::http4s-ember-server:0.23.30
8-
//> using dep com.softwaremill.sttp.client3::async-http-client-backend-zio:3.10.1
8+
//> using dep com.softwaremill.sttp.client3::async-http-client-backend-zio:3.10.2
99

1010
package sttp.tapir.examples
1111

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// {cat=Observability; effects=cats-effect; server=Netty; json=circe}: Otel4s collecting traces
2+
3+
//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.11
4+
//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-cats:1.11.11
5+
//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.11
6+
//> using dep com.softwaremill.sttp.tapir::tapir-opentelemetry-metrics:1.11.11
7+
//> using dep "org.typelevel::otel4s-oteljava:0.11.2"
8+
//> using dep "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:1.45.0"
9+
//> using dep org.slf4j:slf4j-api:2.0.16
10+
11+
package sttp.tapir.examples.observability
12+
13+
import cats.effect
14+
import cats.effect.std.{Console, Random}
15+
import cats.effect.{Async, IO, IOApp}
16+
import io.circe.generic.auto.*
17+
import sttp.tapir.*
18+
import sttp.tapir.generic.auto.*
19+
import sttp.tapir.json.circe.jsonBody
20+
import sttp.tapir.server.netty.cats.NettyCatsServer
21+
import org.slf4j.{Logger, LoggerFactory}
22+
import org.typelevel.otel4s.Attribute
23+
import org.typelevel.otel4s.trace.Tracer
24+
import cats.syntax.all.*
25+
import io.opentelemetry.api.common.Attributes
26+
import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdkBuilder as AutoConfigOtelSdkBuilder
27+
import org.typelevel.otel4s.oteljava.OtelJava
28+
import io.opentelemetry.sdk.resources.Resource
29+
30+
import scala.concurrent.duration.*
31+
import sttp.tapir.server.ServerEndpoint
32+
33+
import scala.io.StdIn
34+
35+
/** This example application demonstrates how to implement distributed tracing in a Scala application using the <a
36+
* href="https://github.com/typelevel/otel4s"> otel4s library </a>. More info about oltel4s you may find <a
37+
* href="https://typelevel.org/otel4s/">here</a>
38+
*
39+
* In order to collect and visualize traces, you can run Jaeger using Docker with the following command:
40+
* {{{
41+
* docker run --name jaeger -e COLLECTOR_OTLP_ENABLED=true -p 16686:16686 -p 4317:4317 -p 4318:4318 jaegertracing/all-in-one:1.35
42+
* }}}
43+
* Jaeger UI is available at http://localhost:16686. You can find the collected traces there.
44+
*/
45+
object Otel4sTracingExample extends IOApp.Simple:
46+
private val logger: Logger = LoggerFactory.getLogger(this.getClass.getName)
47+
48+
override def run: IO[Unit] = otel
49+
.use { otel4s =>
50+
otel4s.tracerProvider
51+
.get("sttp.tapir.examples.observability")
52+
.flatMap { case given Tracer[IO] => server(HttpApi[IO](Service[IO])) }
53+
}
54+
55+
private def server(httpApi: HttpApi[IO]): IO[Unit] =
56+
NettyCatsServer
57+
.io()
58+
.use { server =>
59+
for {
60+
bind <- server
61+
.port(9090)
62+
.host("localhost")
63+
.addEndpoint(httpApi.doWorkServerEndpoint)
64+
.start()
65+
_ <- IO
66+
.blocking {
67+
logger.info(s"""Server started. Try it with: curl -X POST ${bind.hostName}:${bind.port}/work -d '{"steps": 10}'""")
68+
logger.info("Press ENTER key to exit.")
69+
StdIn.readLine()
70+
71+
}
72+
.guarantee(bind.stop())
73+
} yield ()
74+
}
75+
76+
private def otel: effect.Resource[IO, OtelJava[IO]] = {
77+
// We implemented customization here to set service name.
78+
// Please notice in your, production cases could be better to use env variable instead.
79+
// Under following link you may find more details about configuration https://typelevel.org/otel4s/sdk/configuration.html
80+
def customize(a: AutoConfigOtelSdkBuilder): AutoConfigOtelSdkBuilder = {
81+
val customResource = Resource.getDefault.merge(
82+
Resource.create(Attributes.of(io.opentelemetry.api.common.AttributeKey.stringKey("service.name"), "Otel4s-Tracing-Example"))
83+
)
84+
a.addResourceCustomizer((resource, config) => customResource)
85+
a
86+
}
87+
OtelJava.autoConfigured[IO](customize)
88+
}
89+
90+
class Service[F[_]: Async: Tracer: Console]:
91+
def doWork(steps: Int): F[Unit] = {
92+
val step = Tracer[F]
93+
.span("internal-work", Attribute("step", steps.toLong))
94+
.surround {
95+
for {
96+
random <- Random.scalaUtilRandom
97+
delay <- random.nextIntBounded(1000)
98+
_ <- Async[F].sleep(delay.millis)
99+
_ <- Console[F].println("Do work ...")
100+
} yield ()
101+
}
102+
103+
if (steps > 0) step *> doWork(steps - 1) else step
104+
}
105+
106+
class HttpApi[F[_]: Async: Tracer](service: Service[F]):
107+
private case class Work(steps: Int)
108+
109+
val doWorkServerEndpoint: ServerEndpoint[Any, F] = endpoint.post
110+
.in("work")
111+
.in(jsonBody[Work])
112+
.serverLogicSuccess(work => {
113+
Tracer[F].span("DoWorkRequest").use { span =>
114+
span.addEvent("Do the work request received") *>
115+
service.doWork(work.steps) *>
116+
span.addEvent("Finished working.")
117+
}
118+
})

examples/src/main/scala/sttp/tapir/examples/security/OAuth2GithubHttp4sServer.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.11
44
//> using dep com.softwaremill.sttp.tapir::tapir-http4s-server:1.11.11
55
//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.11
6-
//> using dep com.softwaremill.sttp.client3::async-http-client-backend-cats:3.10.1
6+
//> using dep com.softwaremill.sttp.client3::async-http-client-backend-cats:3.10.2
77
//> using dep org.http4s::http4s-ember-server:0.23.30
88
//> using dep com.github.jwt-scala::jwt-circe:10.0.1
99

examples/src/main/scala/sttp/tapir/examples/security/ServerSecurityLogicZio.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.11
44
//> using dep com.softwaremill.sttp.tapir::tapir-zio-http-server:1.11.11
5-
//> using dep com.softwaremill.sttp.client3::async-http-client-backend-zio:3.10.1
5+
//> using dep com.softwaremill.sttp.client3::async-http-client-backend-zio:3.10.2
66

77
package sttp.tapir.examples.security
88

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// {cat=Security; effects=Future; server=Vert.x}: CORS interceptor
2+
3+
//> using dep com.softwaremill.sttp.tapir::tapir-vertx-server:1.11.11
4+
//> using dep com.softwaremill.sttp.client3::core:3.10.2
5+
6+
package sttp.tapir.examples.security
7+
8+
import io.vertx.core.Vertx
9+
import io.vertx.ext.web.*
10+
import sttp.client3.*
11+
import sttp.model.headers.Origin
12+
import sttp.model.{Header, HeaderNames, Method, StatusCode}
13+
import sttp.tapir.*
14+
import sttp.tapir.server.interceptor.cors.{CORSConfig, CORSInterceptor}
15+
import sttp.tapir.server.vertx.VertxFutureServerInterpreter.*
16+
import sttp.tapir.server.vertx.{VertxFutureServerInterpreter, VertxFutureServerOptions}
17+
18+
import scala.concurrent.duration.*
19+
import scala.concurrent.{Await, ExecutionContext, Future}
20+
21+
@main def corsInterceptorVertxServer() =
22+
given ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global
23+
val vertx = Vertx.vertx()
24+
25+
val server = vertx.createHttpServer()
26+
val router = Router.router(vertx)
27+
28+
val myEndpoint = endpoint.get
29+
.in("path")
30+
.out(plainBody[String])
31+
.serverLogic(_ => Future(Right("OK")))
32+
33+
val corsInterceptor = VertxFutureServerOptions.customiseInterceptors
34+
.corsInterceptor(
35+
CORSInterceptor.customOrThrow(
36+
CORSConfig.default
37+
.allowOrigin(Origin.Host("http", "my.origin"))
38+
.allowMethods(Method.GET)
39+
)
40+
)
41+
.options
42+
43+
val attach = VertxFutureServerInterpreter(corsInterceptor).route(myEndpoint)
44+
attach(router)
45+
46+
// starting the server
47+
val bindAndCheck = server.requestHandler(router).listen(9000).asScala.map { binding =>
48+
val backend = HttpClientSyncBackend()
49+
50+
// Sending preflight request with allowed origin
51+
val preflightResponse = basicRequest
52+
.options(uri"http://localhost:9000/path")
53+
.headers(
54+
Header.origin(Origin.Host("http", "my.origin")),
55+
Header.accessControlRequestMethod(Method.GET)
56+
)
57+
.send(backend)
58+
59+
assert(preflightResponse.code == StatusCode.NoContent)
60+
assert(preflightResponse.headers.contains(Header.accessControlAllowOrigin("http://my.origin")))
61+
assert(preflightResponse.headers.contains(Header.accessControlAllowMethods(Method.GET)))
62+
63+
println("Got expected response for preflight request")
64+
65+
// Sending preflight request with disallowed origin
66+
val preflightResponseForDisallowedOrigin = basicRequest
67+
.options(uri"http://localhost:9000/path")
68+
.headers(
69+
Header.origin(Origin.Host("http", "disallowed.com")),
70+
Header.accessControlRequestMethod(Method.GET)
71+
)
72+
.send(backend)
73+
74+
// Check response does not contain allowed origin header
75+
assert(preflightResponseForDisallowedOrigin.code == StatusCode.NoContent)
76+
assert(!preflightResponseForDisallowedOrigin.headers.contains(Header.accessControlAllowOrigin("http://example.com")))
77+
78+
println("Got expected response for preflight request for wrong origin. No allowed origin header in response")
79+
80+
// Sending regular request from allowed origin
81+
val requestResponse = basicRequest
82+
.response(asStringAlways)
83+
.get(uri"http://localhost:9000/path")
84+
.headers(Header.origin(Origin.Host("http", "my.origin")))
85+
.send(backend)
86+
87+
assert(requestResponse.code == StatusCode.Ok)
88+
assert(requestResponse.body == "OK")
89+
assert(requestResponse.headers.contains(Header.vary(HeaderNames.Origin)))
90+
assert(requestResponse.headers.contains(Header.accessControlAllowOrigin("http://my.origin")))
91+
92+
println("Got expected response for regular request")
93+
94+
binding
95+
}
96+
97+
Await.result(bindAndCheck.flatMap(_.close().asScala), 1.minute)

examples/src/main/scala/sttp/tapir/examples/status_code/statusCodeNettyServer.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// {cat=Status code; effects=Direct; server=Netty}: Serving static files from a directory
1+
// {cat=Status code; effects=Direct; server=Netty}: Responding with fixed or dynamic status code
22

33
//> using dep com.softwaremill.sttp.tapir::tapir-core:1.11.11
44
//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.11.11

examples/src/main/scala/sttp/tapir/examples/websocket/WebSocketHttp4sServer.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
//> using dep com.softwaremill.sttp.tapir::tapir-asyncapi-docs:1.11.11
66
//> using dep com.softwaremill.sttp.tapir::tapir-json-circe:1.11.11
77
//> using dep com.softwaremill.sttp.apispec::asyncapi-circe-yaml:0.10.0
8-
//> using dep com.softwaremill.sttp.client3::async-http-client-backend-fs2:3.10.1
9-
//> using dep org.http4s::http4s-ember-server:0.23.16
8+
//> using dep com.softwaremill.sttp.client3::async-http-client-backend-fs2:3.10.2
9+
//> using dep org.http4s::http4s-ember-server:0.23.30
1010

1111
package sttp.tapir.examples.websocket
1212

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
sbt.version=1.10.6
1+
sbt.version=1.10.7
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
sbt.version=1.10.6
1+
sbt.version=1.10.7
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
sbt.version=1.10.6
1+
sbt.version=1.10.7

openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter/build.sbt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ libraryDependencies ++= Seq(
1111
"com.softwaremill.sttp.tapir" %% "tapir-openapi-docs" % "1.10.0",
1212
"com.softwaremill.sttp.apispec" %% "openapi-circe-yaml" % "0.8.0",
1313
"com.beachape" %% "enumeratum" % "1.7.5",
14-
"com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % "2.32.0",
15-
"com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % "2.32.0" % "compile-internal",
14+
"com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % "2.33.0",
15+
"com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % "2.33.0" % "compile-internal",
1616
"org.scalatest" %% "scalatest" % "3.2.19" % Test,
1717
"com.softwaremill.sttp.tapir" %% "tapir-sttp-stub-server" % "1.10.0" % Test
1818
)
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
sbt.version=1.10.6
1+
sbt.version=1.10.7

openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter_scala3/build.sbt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ libraryDependencies ++= Seq(
1111
"com.softwaremill.sttp.tapir" %% "tapir-openapi-docs" % "1.10.0",
1212
"com.softwaremill.sttp.apispec" %% "openapi-circe-yaml" % "0.8.0",
1313
"com.beachape" %% "enumeratum" % "1.7.5",
14-
"com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % "2.32.0",
15-
"com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % "2.32.0" % "compile-internal",
14+
"com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % "2.33.0",
15+
"com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % "2.33.0" % "compile-internal",
1616
"org.scalatest" %% "scalatest" % "3.2.19" % Test,
1717
"com.softwaremill.sttp.tapir" %% "tapir-sttp-stub-server" % "1.10.0" % Test
1818
)
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
sbt.version=1.10.6
1+
sbt.version=1.10.7
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
sbt.version=1.10.6
1+
sbt.version=1.10.7
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
sbt.version=1.10.6
1+
sbt.version=1.10.7

0 commit comments

Comments
 (0)