Skip to content

Commit

Permalink
#91: docs
Browse files Browse the repository at this point in the history
  • Loading branch information
adamw committed Oct 20, 2020
1 parent 4d249ee commit 9171bdd
Show file tree
Hide file tree
Showing 4 changed files with 322 additions and 0 deletions.
72 changes: 72 additions & 0 deletions generated-doc/out/docs/asyncapi.md
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).
130 changes: 130 additions & 0 deletions generated-doc/out/docs/openapi.md
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)
}
```
39 changes: 39 additions & 0 deletions generated-doc/out/endpoint/streaming.md
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).
81 changes: 81 additions & 0 deletions generated-doc/out/endpoint/websockets.md
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).

0 comments on commit 9171bdd

Please sign in to comment.