From 91e985978974ad7504f349ac8fcb2a1369aa9746 Mon Sep 17 00:00:00 2001 From: Sebastian Arellanes <80025691+KapStorm@users.noreply.github.com> Date: Wed, 20 Sep 2023 14:23:02 -0700 Subject: [PATCH] Update swagger docs (#427) --- docs/swagger-integration.md | 148 +++++------------- .../io/swagger/annotations/ApiModel.scala | 18 --- .../annotations/ApiModelProperty.scala | 44 ------ .../io/swagger/annotations/Extension.scala | 17 -- .../annotations/ExtensionProperty.scala | 17 -- 5 files changed, 41 insertions(+), 203 deletions(-) delete mode 100644 lib/api/js/src/main/scala/io/swagger/annotations/ApiModel.scala delete mode 100644 lib/api/js/src/main/scala/io/swagger/annotations/ApiModelProperty.scala delete mode 100644 lib/api/js/src/main/scala/io/swagger/annotations/Extension.scala delete mode 100644 lib/api/js/src/main/scala/io/swagger/annotations/ExtensionProperty.scala diff --git a/docs/swagger-integration.md b/docs/swagger-integration.md index d78b441d..8f7a6d9d 100644 --- a/docs/swagger-integration.md +++ b/docs/swagger-integration.md @@ -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 = "alexis@wiringbits.net" + ) + ) + ) + .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 - ) -} -``` \ No newline at end of file +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/). diff --git a/lib/api/js/src/main/scala/io/swagger/annotations/ApiModel.scala b/lib/api/js/src/main/scala/io/swagger/annotations/ApiModel.scala deleted file mode 100644 index 2385b893..00000000 --- a/lib/api/js/src/main/scala/io/swagger/annotations/ApiModel.scala +++ /dev/null @@ -1,18 +0,0 @@ -package io.swagger.annotations - -import scala.annotation.Annotation - -// Dummy annotation to allow using swagger-annotations in our sjs compiled models -// Based on https://github.com/swagger-api/swagger-core -// -// NOTE: Due to a compiler bug, we must define the values in the order we expect them to be used -// otherwise, we end up with compile warnings - https://github.com/scala/bug/issues/7656 -@SuppressWarnings(Array("org.wartremover.warts.ArrayEquals")) -final case class ApiModel( - value: String = "", - description: String = "", - parent: Class[_] = classOf[Unit], - discriminator: String = "", - subTypes: Array[Class[_]] = Array.empty, - reference: String = "" -) extends Annotation diff --git a/lib/api/js/src/main/scala/io/swagger/annotations/ApiModelProperty.scala b/lib/api/js/src/main/scala/io/swagger/annotations/ApiModelProperty.scala deleted file mode 100644 index 89902826..00000000 --- a/lib/api/js/src/main/scala/io/swagger/annotations/ApiModelProperty.scala +++ /dev/null @@ -1,44 +0,0 @@ -/** Copyright 2016 SmartBear Software

Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of the License at

- * http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, - * either express or implied. See the License for the specific language governing permissions and limitations under the - * License. - */ - -package io.swagger.annotations - -// Dummy annotation to allow using swagger-annotations in our sjs compiled models -// Based on https://github.com/swagger-api/swagger-core -// -// NOTE: Due to a compiler bug, we must define the values in the order we expect them to be used -// otherwise, we end up with compile warnings - https://github.com/scala/bug/issues/7656 -@SuppressWarnings(Array("org.wartremover.warts.ArrayEquals")) -case class ApiModelProperty( - value: String = "", - dataType: String = "", - example: String = "", - name: String = "", - allowableValues: String = "", - access: String = "", - notes: String = "", - required: Boolean = false, - position: Int = 0, - hidden: Boolean = false, - readOnly: Boolean = false, - accessMode: ApiModelProperty.AccessMode.AccessMode = ApiModelProperty.AccessMode.AUTO, - reference: String = "", - allowEmptyValue: Boolean = false, - extensions: Array[Extension] = Array( - Extension(properties = Array(ExtensionProperty(name = "", value = ""))) - ) -) extends scala.annotation.Annotation - -/** Adds and manipulates data of a model property. - */ -object ApiModelProperty { - object AccessMode extends Enumeration { - type AccessMode = Value - val AUTO, READ_ONLY, READ_WRITE = Value - } -} diff --git a/lib/api/js/src/main/scala/io/swagger/annotations/Extension.scala b/lib/api/js/src/main/scala/io/swagger/annotations/Extension.scala deleted file mode 100644 index bfe987b2..00000000 --- a/lib/api/js/src/main/scala/io/swagger/annotations/Extension.scala +++ /dev/null @@ -1,17 +0,0 @@ -/** Copyright 2016 SmartBear Software

Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of the License at

- * http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, - * either express or implied. See the License for the specific language governing permissions and limitations under the - * License. - */ - -package io.swagger.annotations - -// Dummy annotation to allow using swagger-annotations in our sjs compiled models -// Based on https://github.com/swagger-api/swagger-core -// -// NOTE: Due to a compiler bug, we must define the values in the order we expect them to be used -// otherwise, we end up with compile warnings - https://github.com/scala/bug/issues/7656 -@SuppressWarnings(Array("org.wartremover.warts.ArrayEquals")) -case class Extension(name: String = "", properties: Array[ExtensionProperty]) extends scala.annotation.Annotation diff --git a/lib/api/js/src/main/scala/io/swagger/annotations/ExtensionProperty.scala b/lib/api/js/src/main/scala/io/swagger/annotations/ExtensionProperty.scala deleted file mode 100644 index 73056781..00000000 --- a/lib/api/js/src/main/scala/io/swagger/annotations/ExtensionProperty.scala +++ /dev/null @@ -1,17 +0,0 @@ -/** Copyright 2016 SmartBear Software

Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of the License at

- * http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, - * either express or implied. See the License for the specific language governing permissions and limitations under the - * License. - */ - -package io.swagger.annotations - -// Dummy annotation to allow using swagger-annotations in our sjs compiled models -// Based on https://github.com/swagger-api/swagger-core -// -// NOTE: Due to a compiler bug, we must define the values in the order we expect them to be used -// otherwise, we end up with compile warnings - https://github.com/scala/bug/issues/7656 -case class ExtensionProperty(name: String, value: String, parseValue: Boolean = false) - extends scala.annotation.Annotation