Skip to content

Commit

Permalink
Update swagger docs (#427)
Browse files Browse the repository at this point in the history
  • Loading branch information
KapStorm authored Sep 20, 2023
1 parent c707737 commit 91e9859
Show file tree
Hide file tree
Showing 5 changed files with 41 additions and 203 deletions.
148 changes: 41 additions & 107 deletions docs/swagger-integration.md
Original file line number Diff line number Diff line change
@@ -1,134 +1,68 @@
[//]: # (TODO: update swagger integration docs)

# Swagger integration

We have a swagger integration so that users can explore the server API through swagger-ui.

**Disclaimer**: This integration can be annoying to work with, it uses Java annotations which end up with lots of repetitive code.

Still, if you decide that swagger is worth for your project, this document explains the known quirks.

Some highlights:

- We are using [sbt-swagger-play](https://github.com/dwickern/sbt-swagger-play) which integrates a [swagger-play](https://github.com/dwickern/swagger-play) fork, while these modules are out of date, they work for most common scenarios.
- [Swagger-Annotations 1.5.x](https://github.com/swagger-api/swagger-core/wiki/Annotations-1.5.X) are the supported annotations.
- This swagger version does not support cookie-based authentication, while this prevents you from specifying cookies at `SecurityDefinition`, you can still invoke the login endpoint from swagger-ui so that the cookie gets propagated by the browser.
- Swagger-ui is exposed locally at [localhost:9000/docs/index.html](http://localhost:9000/docs/index.html)
- Avoid adding quite a lot of swagger annotation changes at once, when there are problems with those, the error could not be obvious, you are encouraged to check swagger-ui after updating an API.
- Be sure to check existing [controllers](../server/src/main/scala/controllers/) to see real examples.
- Swagger belongs to the http layer, hence, swagger annotations must be only at the [controllers](../server/src/main/scala/controllers/) and [api-models](../lib/api/shared/src/main/scala/net/wiringbits/api/models/) packages.
- Returning arrays can be tricky, reflection notation is required.
- `Option[T]` values need explicit types so that swagger definition is accurate (the default inferred type is wrong).

**NOTE**: Swagger errors are not clear, check previous highlights, and, refresh swagger-ui frequently to make sure everything works the way you expect.

## Expose a controller
Controllers are not exposed by default, in order to do so, you need to annotate your controller with `@Api`, for example:

```scala
@Api("Auth")
class AuthController
```

In this case, `Auth` is the tag for the APIs exposed on `AuthController`, such tag is used to group the APIs on swagger-ui.
- We are using [tapir](https://tapir.softwaremill.com/) which integrates an [open-api](https://tapir.softwaremill.com/en/latest/docs/openapi.html) module for swagger.
- Swagger-ui is exposed locally at [http://localhost:9000/docs](http://localhost:9000/docs).
- Be sure to check existing [endpoints](../lib/api/shared/src/main/scala/net/wiringbits/api/endpoints) to see real examples.
- `Option[T]` values are supported, sending a json without the key and value will be interpreted as `None`, otherwise, `Some(value)` will be sent.

**NOTE**: swagger-play will match the controller methods to the endpoints defined at the [routes](../server/src/main/resources/routes) file.

## Controller authentication details
Any controller exposing APIs requiring user authentication must be annotated with `@SwaggerDefinition` to include the available security definitions, you will find yourself mostly writing this once and pasting it on all controllers, for example:
## Creating an endpoint definition
We have to define our endpoints at the [endpoints](../lib/api/shared/src/main/scala/net/wiringbits/api/endpoints) package, for example:

```scala
@SwaggerDefinition(
securityDefinition = new SecurityDefinition(
apiKeyAuthDefinitions = Array(
new ApiKeyAuthDefinition(
name = "Cookie",
key = "auth_cookie",
in = ApiKeyAuthDefinition.ApiKeyLocation.HEADER,
description =
"The user's session cookie retrieved when logging into the app, invoke the login API to get the cookie stored in the browser"
val basicPostEndpoint = endpoint
.post("basic") // points to POST http://localhost:9000/basic
.tag("Misc") // tags the endpoint as "Misc" on swagger-ui
.in(
jsonBody[Basic.Request].example( // expects a JSON body of type BasicGet.Request with example values
BasicGet.Request(
name = "Alexis",
email = "[email protected]"
)
)
)
.out(
jsonBody[Basic.Response].example( // returns a JSON body of type BasicGet.Response with example values
BasicGet.Response(
message = "Hello Alexis!"
)
)
)
)
@Api("Auth")
class AuthController
```

This means that any endpoint can define the auth-definition key to mark the endpoint as protected, in this case `auth_cookie`, for example:
Api models must have an `implicit Schema` defined, for example:

```scala
@ApiOperation(
value = "Logout from the app",
notes = "Clears the session cookie that's stored securely",
authorizations = Array(new Authorization(value = "auth_cookie"))
)
def logout = ???
Schema
.derived[Response]
.name(Schema.SName("BasicResponse"))
.description("Says hello to the user")
```

## Controller methods
A controller method would usually look like this (removing pieces when necessary):
And then integrate the endpoint to the [ApiRouter](../server/src/main/scala/controllers/ApiRouter.scala) file:

```scala
@ApiOperation(
value = "Logout from the app",
notes = "Clears the session cookie that's stored securely",
authorizations = Array(new Authorization(value = "auth_cookie"))
)
@ApiImplicitParams(
Array(
new ApiImplicitParam(
name = "body",
value = "JSON-encoded request",
required = true,
paramType = "body",
dataTypeClass = classOf[Logout.Request]
)
)
)
@ApiResponses(
Array(
new ApiResponse(code = 200, message = "Successful logout", response = classOf[Logout.Response]),
new ApiResponse(code = 400, message = "Invalid or missing arguments")
)
object ApiRouter {
private def routes(implicit ec: ExecutionContext): List[AnyEndpoint] = List(
basicPostEndpoint
)
}
```

- `ApiOperation` defines the summary for the API, including authentication details when necessary.
- `ApiImplicitParams` defines the request body type (such class needs its own swagger-annotations).
- `ApiResponses` defines the potential responses for this method.

## Endpoint user authentication details

## Annotations on models
We use Play Session cookie for user authentication, this is a cookie that's stored securely and is sent on every request, this cookie is used to identify the user and to check if the user is authenticated.

This is one of the most tricky side from this integration:

- We commonly use classes nested inside objects to define request/response models, we need to declare explicit swagger names for these models.
- Wrapper values get default weird values in swagger, for example, `Option[T]`, our typed models like `class Email private (val string: String) extends WrappedString`, hence, swagger needs an explicit type defined.
- Arrays get weird values too by default, reflection notation needs to be used for these.
- `ApiModel`/`ApiModelProperty` annotation parameters ordering matters! This is one of the more obscure details, if you get any weird error, check the annotation parameter ordering.
- Primitive values work fine.

Let's see an example:
Any endpoint that requieres user authentication must include our implicit [userAuth](../lib/api/shared/src/main/scala/net/wiringbits/api/endpoints/package.scala) handler and convert the endpoint `val` to `def` that receives an implicit handler `implicit
authHandler: ServerRequest => Future[UUID]`, for example:

[//]: # (TODO: change Future[UUID] to Future[UserId] after mergin typo)
```scala
object CreateUser {
@ApiModel(value = "CreateUserRequest", description = "Request for the create user API")
case class Request(
@ApiModelProperty(dataType = "string")
email: Email,
@ApiModelProperty(dataType = "[Ljava.lang.Long;")
longSeqOpt: Option[Seq[Long]],
@ApiModelProperty(dataType = "[Ljava.lang.String;")
stringSeq: Seq[String],
@ApiModelProperty(dataType = "integer")
intOpt: Option[Int],
@ApiModelProperty(dataType = "boolean")
booleanOpt: Option[Boolean],
int: Int,
boolean: Boolean,
long: Long,
string: String
)
}
```
def basicEndpoint(implicit authHandler: ServerRequest => Future[UUID]) = endpoint.get
.in(userAuth)
```

For more information about creating endpoints, please check the [tapir documentation](https://tapir.softwaremill.com/en/latest/).
18 changes: 0 additions & 18 deletions lib/api/js/src/main/scala/io/swagger/annotations/ApiModel.scala

This file was deleted.

This file was deleted.

17 changes: 0 additions & 17 deletions lib/api/js/src/main/scala/io/swagger/annotations/Extension.scala

This file was deleted.

This file was deleted.

1 comment on commit 91e9859

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preview ready at https://master.sssppa.wiringbits.dev

Powered by https://codepreview.io community edition.

Please sign in to comment.