-
Notifications
You must be signed in to change notification settings - Fork 423
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
322 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
# Generating AsyncAPI documentation | ||
|
||
To use, add the following dependencies: | ||
|
||
```scala | ||
"com.softwaremill.sttp.tapir" %% "tapir-asyncapi-docs" % "0.17.0-M2" | ||
"com.softwaremill.sttp.tapir" %% "tapir-asyncapi-circe-yaml" % "0.17.0-M2" | ||
``` | ||
|
||
Tapir contains a case class-based model of the asyncapi data structures in the `asyncapi/asyncapi-model` subproject (the | ||
model is independent from all other tapir modules and can be used stand-alone). | ||
|
||
An endpoint can be converted to an instance of the model by importing the `sttp.tapir.docs.asyncapi._` package and calling | ||
the provided extension method: | ||
|
||
```scala | ||
import sttp.capabilities.akka.AkkaStreams | ||
import sttp.tapir._ | ||
import sttp.tapir.asyncapi.AsyncAPI | ||
import sttp.tapir.docs.asyncapi._ | ||
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)) | ||
|
||
val docs: AsyncAPI = echoWS.toAsyncAPI("Echo web socket", "1.0") | ||
``` | ||
|
||
Such a model can then be refined, by adding details which are not auto-generated. Working with a deeply nested case | ||
class structure such as the `AstncAPI` one can be made easier by using a lens library, e.g. [Quicklens](https://github.com/adamw/quicklens). | ||
|
||
The documentation is generated in a large part basing on [schemas](endpoint/codecs.md#schemas). Schemas can be | ||
[automatically derived and customised](endpoint/customtypes.md#schema-derivation). | ||
|
||
Quite often, you'll need to define the servers, through which the API can be reached. Any servers provided to the | ||
`.toAsyncAPI` invocation will be supplemented with security requirements, as specified by the endpoints: | ||
|
||
```scala | ||
import sttp.tapir.asyncapi.Server | ||
|
||
val docsWithServers: AsyncAPI = echoWS.toAsyncAPI( | ||
"Echo web socket", | ||
"1.0", | ||
List("production" -> Server("api.example.com", "wss")) | ||
) | ||
``` | ||
|
||
Servers can also be later added through methods on the `AsyncAPI` object. | ||
|
||
Multiple endpoints can be converted to an `AsyncAPI` instance by calling the extension method on a list of endpoints/ | ||
|
||
The asyncapi case classes can then be serialised, either to JSON or YAML using [Circe](https://circe.github.io/circe/): | ||
|
||
```scala | ||
import sttp.tapir.asyncapi.circe.yaml._ | ||
|
||
println(docs.toYaml) | ||
``` | ||
|
||
## Options | ||
|
||
Options can be customised by providing an implicit instance of `AsyncAPIDocsOptions`, when calling `.toAsyncAPI`. | ||
|
||
* `subscribeOperationId`: basing on the endpoint's path and the entire endpoint, determines the id of the subscribe | ||
operation. This can be later used by code generators as the name of the method to receive messages from the socket. | ||
* `publishOperationId`: as above, but for publishing (sending messages to the web socket). | ||
|
||
## Exposing AsyncAPI documentation | ||
|
||
AsyncAPI documentation can be exposed through the [AsyncAPI playground](https://playground.asyncapi.io). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
# Generating OpenAPI documentation | ||
|
||
To use, add the following dependencies: | ||
|
||
```scala | ||
"com.softwaremill.sttp.tapir" %% "tapir-openapi-docs" % "0.17.0-M2" | ||
"com.softwaremill.sttp.tapir" %% "tapir-openapi-circe-yaml" % "0.17.0-M2" | ||
``` | ||
|
||
Tapir contains a case class-based model of the openapi data structures in the `openapi/openapi-model` subproject (the | ||
model is independent from all other tapir modules and can be used stand-alone). | ||
|
||
An endpoint can be converted to an instance of the model by importing the `sttp.tapir.docs.openapi._` package and calling | ||
the provided extension method: | ||
|
||
```scala | ||
import sttp.tapir._ | ||
import sttp.tapir.openapi.OpenAPI | ||
import sttp.tapir.docs.openapi._ | ||
|
||
val booksListing = endpoint.in(path[String]("bookId")) | ||
|
||
val docs: OpenAPI = booksListing.toOpenAPI("My Bookshop", "1.0") | ||
``` | ||
|
||
Such a model can then be refined, by adding details which are not auto-generated. Working with a deeply nested case | ||
class structure such as the `OpenAPI` one can be made easier by using a lens library, e.g. [Quicklens](https://github.com/adamw/quicklens). | ||
|
||
The documentation is generated in a large part basing on [schemas](endpoint/codecs.md#schemas). Schemas can be | ||
[automatically derived and customised](endpoint/customtypes.md#schema-derivation). | ||
|
||
Quite often, you'll need to define the servers, through which the API can be reached. To do this, you can modify the | ||
returned `OpenAPI` case class either directly or by using a helper method: | ||
|
||
```scala | ||
import sttp.tapir.openapi.Server | ||
|
||
val docsWithServers: OpenAPI = booksListing.toOpenAPI("My Bookshop", "1.0") | ||
.servers(List(Server("https://api.example.com/v1").description("Production server"))) | ||
``` | ||
|
||
Multiple endpoints can be converted to an `OpenAPI` instance by calling the extension method on a list of endpoints: | ||
|
||
|
||
```scala | ||
List(addBook, booksListing, booksListingByGenre).toOpenAPI("My Bookshop", "1.0") | ||
``` | ||
|
||
The openapi case classes can then be serialised, either to JSON or YAML using [Circe](https://circe.github.io/circe/): | ||
|
||
```scala | ||
import sttp.tapir.openapi.circe.yaml._ | ||
|
||
println(docs.toYaml) | ||
``` | ||
|
||
## Options | ||
|
||
Options can be customised by providing an implicit instance of `OpenAPIDocsOptions`, when calling `.toOpenAPI`. | ||
|
||
* `operationIdGenerator`: each endpoint corresponds to an operation in the OpenAPI format and should have a unique | ||
operation id. By default, the `name` of endpoint is used as the operation id, and if this is not available, the | ||
operation id is auto-generated by concatenating (using camel-case) the request method and path. | ||
|
||
## Exposing OpenAPI documentation | ||
|
||
Exposing the OpenAPI documentation can be very application-specific. However, tapir contains modules which contain | ||
akka-http/http4s routes for exposing documentation using [Swagger UI](https://swagger.io/tools/swagger-ui/) or | ||
[Redoc](https://github.com/Redocly/redoc): | ||
|
||
```scala | ||
// Akka HTTP | ||
"com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-akka-http" % "0.17.0-M2" | ||
"com.softwaremill.sttp.tapir" %% "tapir-redoc-akka-http" % "0.17.0-M2" | ||
|
||
// Finatra | ||
"com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-finatra" % "0.17.0-M2" | ||
|
||
// HTTP4S | ||
"com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-http4s" % "0.17.0-M2" | ||
"com.softwaremill.sttp.tapir" %% "tapir-redoc-http4s" % "0.17.0-M2" | ||
|
||
// Play | ||
"com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-play" % "0.17.0-M2" | ||
"com.softwaremill.sttp.tapir" %% "tapir-redoc-play" % "0.17.0-M2" | ||
``` | ||
|
||
Note: `tapir-swagger-ui-akka-http` transitively pulls some Akka modules in version 2.6. If you want to force | ||
your own Akka version (for example 2.5), use sbt exclusion. Mind the Scala version in artifact name: | ||
|
||
```scala | ||
"com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-akka-http" % "0.17.0-M2" exclude("com.typesafe.akka", "akka-stream_2.12") | ||
``` | ||
|
||
Usage example for akka-http: | ||
|
||
```scala | ||
import sttp.tapir._ | ||
import sttp.tapir.docs.openapi._ | ||
import sttp.tapir.openapi.circe.yaml._ | ||
import sttp.tapir.swagger.akkahttp.SwaggerAkka | ||
|
||
val myEndpoints: Seq[Endpoint[_, _, _, _]] = ??? | ||
val docsAsYaml: String = myEndpoints.toOpenAPI("My App", "1.0").toYaml | ||
// add to your akka routes | ||
new SwaggerAkka(docsAsYaml).routes | ||
``` | ||
|
||
For redoc, use `RedocAkkaHttp`. | ||
|
||
For http4s, use the `SwaggerHttp4s` or `RedocHttp4s` classes. | ||
|
||
For Play, use `SwaggerPlay` or `RedocPlay` classes. | ||
|
||
### Using with sbt-assembly | ||
|
||
The `tapir-swagger-ui-*` modules rely on a file in the `META-INF` directory tree, to determine the version of the Swagger UI. | ||
You need to take additional measures if you package your application with [sbt-assembly](https://github.com/sbt/sbt-assembly) | ||
because the default merge strategy of the `assembly` task discards most artifacts in that directory. | ||
To avoid a `NullPointerException`, you need to include the following file explicitly: | ||
|
||
```scala | ||
assemblyMergeStrategy in assembly := { | ||
case PathList("META-INF", "maven", "org.webjars", "swagger-ui", "pom.properties") => | ||
MergeStrategy.singleOrError | ||
case x => | ||
val oldStrategy = (assemblyMergeStrategy in assembly).value | ||
oldStrategy(x) | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
# Streaming support | ||
|
||
Both input and output bodies can be mapped to a stream, by using `streamBody(streams)`. The parameter `streams` must | ||
implement the `Streams[S]` capability, and determines the precise type of the binary stream supported by the given | ||
non-blocking streams implementation. The interpreter must then support the given capability. Refer to the documentation | ||
of server/client interpreters for more information. | ||
|
||
```eval_rst | ||
.. note:: | ||
Here, streams refer to asynchronous, non-blocking, "reactive" stream implementations, such as `akka-streams <https://doc.akka.io/docs/akka/current/stream/index.html>`_, | ||
`fs2 <https://fs2.io>`_ or `zio-streams <https://zio.dev/docs/datatypes/datatypes_stream>`_. If you'd like to use | ||
blocking streams (such as ``InputStream``), these are available through e.g. ``inputStreamBody`` without any | ||
additional requirements on the interpreter. | ||
``` | ||
|
||
Adding a stream body input/output influences both the type of the input/output, as well as the 4th type parameter | ||
of `Endpoint`, which specifies the requirements regarding supported stream types for interpreters. | ||
|
||
When using a stream body, the schema (needed for documentation) and format (media type) of the body must be provided by | ||
hand, as they cannot be inferred from the raw stream type. For example, to specify that the output is an akka-stream, | ||
which is a (presumably large) serialised list of json objects mapping to the `Person` class: | ||
|
||
```scala | ||
import sttp.tapir._ | ||
import sttp.capabilities.akka.AkkaStreams | ||
import akka.stream.scaladsl._ | ||
import akka.util.ByteString | ||
|
||
case class Person(name: String) | ||
|
||
endpoint.out(streamBody(AkkaStreams, schemaFor[List[Person]], CodecFormat.Json())) | ||
``` | ||
|
||
See also the [runnable streaming example](../examples.md). | ||
|
||
## Next | ||
|
||
Read on about [web sockets](websockets.md). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
# Web sockets | ||
|
||
Web sockets are supported through stream pipes, converting a stream of incoming messages to a stream of outgoing | ||
messages. That's why web socket endpoints require both the `Streams` and `WebSocket` capabilities (see | ||
[streaming support](streaming.md) for more information on streams). | ||
|
||
## Typed web sockets | ||
|
||
Web sockets outputs can be used in two variants. In the first, both requests and responses are handled by | ||
[codecs](codecs.md). Typically, a codec handles either text or binary messages, signalling decode failure (which | ||
closes the web socket), if an unsupported frame is passed to decoding. | ||
|
||
For example, here's an endpoint where the requests are strings (hence only text frames are handled), and the responses | ||
are parsed/formatted as json: | ||
|
||
```scala | ||
import sttp.tapir._ | ||
import sttp.capabilities.akka.AkkaStreams | ||
import sttp.tapir.json.circe._ | ||
import io.circe.generic.auto._ | ||
|
||
case class Response(msg: String, count: Int) | ||
endpoint.out( | ||
webSocketBody[String, CodecFormat.TextPlain, Response, CodecFormat.Json](AkkaStreams)) | ||
``` | ||
|
||
When creating a `webSocketBody`, we need to provide the following parameters: | ||
* the type or requests, along with its codec format (which is used to lookup the appropriate codec, as well as | ||
determines the media type in documentation) | ||
* the type of responses, along with its codec format | ||
* the `Streams` implementation, which determines the pipe type | ||
|
||
By default, ping-pong frames are handled automatically, fragmented frames are combined, and close frames aren't | ||
decoded, but this can be customised through methods on `webSocketBody`. | ||
|
||
## Raw web sockets | ||
|
||
Alternatively, it's possible to obtain a raw pipe transforming `WebSocketFrame`s: | ||
|
||
```scala | ||
import akka.stream.scaladsl.Flow | ||
import sttp.tapir._ | ||
import sttp.capabilities.akka.AkkaStreams | ||
import sttp.capabilities.WebSockets | ||
import sttp.ws.WebSocketFrame | ||
|
||
endpoint.out(webSocketBodyRaw(AkkaStreams)): Endpoint[ | ||
Unit, | ||
Unit, | ||
Flow[WebSocketFrame, WebSocketFrame, Any], | ||
AkkaStreams with WebSockets] | ||
``` | ||
|
||
Such a pipe by default doesn't handle ping-pong frames automatically, doesn't concatenate fragmented flames, and | ||
passes close frames to the pipe as well. As before, this can be customised by methods on the returned output. | ||
|
||
Request/response schemas can be customised through `.requestsSchema` and `.responsesSchema`. | ||
|
||
## 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]`. | ||
|
||
## Interpreting as a client | ||
|
||
When interpreting a web socket endpoint as a client, after applying the input parameters, the result is a pipe | ||
representing message processing as it happens on the server. | ||
|
||
## Interpreting as documentation | ||
|
||
Web socket endpoints can be interpreted into [AsyncAPI documentation](../docs/asyncapi.md). | ||
|
||
## Determining if the request is a web socket upgrade | ||
|
||
The `isWebSocket` endpoint input can be used to determine if the request contains the web socket upgrade headers. | ||
The input only impacts server interpreters, doesn't affect documentation and its value is discarded by client | ||
interpreters. | ||
|
||
## Next | ||
|
||
Read on about [datatypes integrations](integrations.md). |