
В данной статье я хочу рассказать, как использовать Swagger модуль для Play Framework, с примерами из реальной жизни. Я расскажу:
- Как прикрутить последнюю версию Swagger-Play (модуль Play, позволяющий использовать аннотации swagger-api и генерировать на их основе документацию в соответствии со спецификацией OpenAPI) и как настроить swagger-ui (библиотеку javascript, служащую для визуализации сгенерированной документации)
- Опишу основные аннотации Swagger-Core и расскажу об особенностях их использования для Scala
- Расскажу, как правильно работать с классами моделей данных
- Как обойти проблему обобщенных типов в Swagger, который не умеет работать с дженериками
- Как научить Swagger понимать ADT (алгебраические типы данных)
- Как описывать коллекции
Статья будет интересна всем, кто использует Play Framework на Scala и собирается автоматизировать документирование API.
Добавление зависимости
Изучив множество источников в интернете делаю вывод, что для того, чтобы подружить Swagger и Play Framework, нужно установить модуль Swagger Play2.
Адрес библиотеки на гитхабе:
https://github.com/swagger-api/swagger-play
Добавляем зависимость:
libraryDependencies ++= Seq(
  "io.swagger" %% "swagger-play2" % "2.0.1-SNAPSHOT"
)
И здесь возникает проблема:
На момент написания этой статьи зависимость не подтягивалась ни из Maven-central, ни из Sonatype репозиториев.
В Maven-central все найденные сборки заканчивалис на Scala 2.12. Вообще не было ни одной собранной версии для Scala 2.13.
Очень надеюсь, что в будущем они появятся.
Полазив по репозиторию Sonatype-releases, я нашел актуальный форк этой библиотеки. Адрес на github:
https://github.com/iterable/swagger-play
Итак, вставляем зависимость:
libraryDependencies ++= Seq(
  "com.iterable" %% "swagger-play" % "2.0.1"
)
Добавляем репозиторий Sonatype:
resolvers += Resolver.sonatypeRepo("releases")
(Не обязательно, т.к. данная сборка в есть Maven-central)
Теперь осталось активировать модуль в конфигурационном файле application.conf
play.modules.enabled += "play.modules.swagger.SwaggerModule"
а также добавить маршрут в routes:
GET     /swagger.json           controllers.ApiHelpController.getResources
И модуль готов к работе.
Теперь модуль Swagger Play будет генерировать json-файл, который можно просматривать в браузере.
Чтобы полностью насладиться возможностями Swagger, нужно также загрузить библиотеку визуализации: swagger-ui. Она предоставляет удобный графический интерфейс для чтения файла swagger.json, а также дает возможность отправлять rest-запросы на сервер, предоставляя отличную альтернативу Postman, Rest-client и другим аналогичным инструментам.
Итак, добавляем в зависимости:
libraryDependencies += "org.webjars" % "swagger-ui" % "3.25.3"
В контроллере создаем метод, перенаправляющий вызовы на статический файл index.html библиотеки:
def redirectDocs: Action[AnyContent] = Action {
    Redirect(
       url = "/assets/lib/swagger-ui/index.html",
       queryStringParams = Map("url" -> Seq("/swagger.json")))
  }
Ну и прописываем маршрут в файле routes:
GET   /docs                   controllers.HomeController.redirectDocs()
Разумеется, необходимо подключить библиотеку webjars-play. Добавляем в зависимости:
libraryDependencies +=  "org.webjars" %% "webjars-play" % "2.8.0"
И добавляем в файл routes маршрут:
GET     /assets/*file               controllers.Assets.at(path="/public", file)
При условии, что наше приложение запущено, набираем в браузере
http://localhost:9000/docs
и, если все сделано правильно, попадаем на страницу swagger нашего приложения:

Страница пока не содержит данных о нашем rest-api. Для того, чтобы это изменить, необходимо использовать аннотации, который будут отсканированы модулем Swagger-Play.
Аннотации
Подробное описание всех аннотаций swagger-api-core можно посмотреть по ссылке:
https://github.com/swagger-api/swagger-core/wiki/Annotations-1.5.X
В своем проекте я использовал следующие аннотации:
@Api  — отмечает класс контроллера как ресурс Swagger (для сканирования)@ApiImplicitParam — описывает «неявный» параметр (например, заданный в теле запроса)@ApiImplicitParams — служит контейнером для нескольких аннотаций @ApiImplicitParam@ApiModel — позволяет описать модель данных@ApiModelProperty — описывает и интерпретирует поле класса модели данных@ApiOperation — описывает метод контроллера (наверное, главная аннотация в этом списке)@ApiParam — описывает параметр запроса, заданный явным образом (в строке запроса, например)@ApiResponse — описывает ответ сервера на запрос@ApiResponses  — служит контейнером для нескольких аннотаций @ApiResponse. Обычно включает дополнительные ответы (например, при возникновении кодов ошибок). Успешный ответ обычно описывается в аннотации @ApiOperationИтак, для того, чтобы Swagger отсканировал класс контроллера, необходимо добавить аннотацию
@Api@Api(value = «RestController», produces = «application/json»)
class RestController @Inject()(Этого достаточно, чтобы Swagger нашел в файле routes маршруты, относящиеся к методам контроллера и попытался описать их.

Но просто указать Swagger класс контроллера явно не достаточно. Swagger ждет от нас подсказок в виде других аннотаций.
Почему Swagger не может это сделать автоматически? Потому что он понятия не имеет, как сериализуются наши классы. В этом проекте я использую uPickle, кто-то использует Circe, кто-то Play-JSON. Поэтому необходимо дать ссылки на получаемые и выдаваемые классы.
Поскольку используемая библиотека написана на Java, в проекте на Scala возникает множество нюансов.
И первое, с чем придется столкнуться — это синтаксис: не работают вложенные аннотации
Например, Java код:
@ApiResponses(value = {
      @ApiResponse(code = 400, message = "Invalid ID supplied"),
      @ApiResponse(code = 404, message = "Pet not found") })
В Scala будет выглядеть так:
@ApiResponses(value = Array(
      new ApiResponse(code = 400, message = "Invalid ID supplied"),
      new ApiResponse(code = 404, message = "Pet not found") ))
Пример 1
Итак, давайте опишем метод контроллера, который ищет сущность в базе данных:
def find(id: String): Action[AnyContent] = 
    safeAction(AllowRead(DrillObj)).async { implicit request =>
      drillsDao.findById(UUID.fromString(id))
        .map(x => x.fold(NotFound(s"Drill with id=$id not found"))(x => 
            Ok(write(x)))).recover(errorsPf)
    }      
При помощи аннотаций мы можем задать описание метода, входящий параметр, получаемый из строки запроса, а также ответы с сервера. В случае успеха, метод отдаст экземпляр класса Drill:
 @ApiOperation(
    value = "Найти тренировоку",
    response = classOf[Drill]
  )
  @ApiResponses(value = Array(
    new ApiResponse(code = 404, message = "Drill with id=$id not found")
  ))
  def find(@ApiParam(value = "String rep of UUID, id тренировки") id: String)=
    safeAction(AllowRead(DrillObj)).async { implicit request =>
      drillsDao.findById(UUID.fromString(id))
        .map(x => x.fold(NotFound(s"Drill with id=$id not found"))(x =>
          Ok(write(x)))).recover(errorsPf)
    }

Мы получили хорошее описание. Swagger почти угадал, во что сериализуется объект, за одним исключением: поля start и end в нашем классе Drill являются объектами класса Instant, и сериализуются в Long. Хотелось бы 0 заменить на более подходящие значения. Мы это можем сделать, применив аннотации @ApiModel, @ApiModelProperty к нашему классу:
@ApiModel
case class Drill(
                id: UUID,
                name: String,
                @ApiModelProperty(
                  dataType = "Long",
                  example = "1585818000000"
                )
                start: Instant,
                @ApiModelProperty(
                  dataType = "Long",
                  example = "1585904400000"
                )
                end: Option[Instant],
                isActive: Boolean
                )
Теперь у нас есть абсолютно корректное описание модели:

Пример 2
Для описание метода Post, где входящий параметр передается в теле запроса используется аннотация
@ApiImplicitParams: @ApiOperation(value = "Новая тренировка")
  @ApiImplicitParams(Array(
    new ApiImplicitParam(
      value = "Новая тренировка",
      required = true,
      dataTypeClass = classOf[Drill],
      paramType = "body"
    )
  ))
  @ApiResponses(value = Array(
    new ApiResponse(code = 200, message = "ok")
  ))
  def insert() = safeAction(AllowWrite(DrillObj)).async { implicit request =>
Пример 3
Пока все было просто. Вот более сложный пример. Допустим, есть обобщенный класс, зависящий от параметра типа:
case class SessionedResponse[T](
                            val ses: SessionData,
                            val payload: T
                          )
Swagger не понимает дженерики, пока, по крайней мере. Мы не можем указать в аннотации:
@ApiOperation(
    value = "Список тренировок",
    response = classOf[SessionedResponse[Drill]]
  )
Единственный путь в такой ситуации, это сделать подкласс от обобщенного типа для каждого из необходимых нам типов. Например, мы могли бы сделать подкласс DrillSessionedResponse.
Единственная беда, мы не можем наследовать от case-класса. К счастью, в моем проекте мне ничего не мешает изменить case class на class. Тогда:
class SessionedResponse[T](
                            val ses: SessionData,
                            val payload: T
                          )
object SessionedResponse {
  def apply[T](ses: SessionData, payload: T) = new SessionedResponse[T](ses, payload)
 
}
private[controllers] class DrillSessionedResponse(
          ses: SessionData,
          payload: List[Drill]
) extends SessionedResponse[List[Drill]](ses, payload)
Теперь я могу указать этот класс в аннотации:
@ApiOperation(
    value = "Список тренировок",
    response = classOf[DrillSessionedResponse]
  )
Пример 4
Теперь еще более сложный пример, связанный с ADT — алгебраическими типами данных.
В Swagger предусмотрен механизм работы с ADT:
Аннотация
@ApiModel имеет 2 параметра для этой цели:1. subTypes — перечисление подклассов
2. discriminator — поле, по которому подклассы отличаются друг от друга.
В моем случае, uPickle производя JSON из case-классов, сам добавляет поле $type, a case — объекты сериализует в строки. Так что подход с полем discriminator оказался неприемлем.
Я использовал другой подход. Допустим, есть
sealed trait Permission
case class Write(obj: Obj) extends Permission
case class Read(obj: Obj) extends Permission
где Obj — это другой ADT, состоящий из case объектов:
//сериализуется в permission.drill
case object DrillObj extends Obj
//сериализуется permission.team
case object TeamObj extends Obj
Чтобы Swagger смог понять эту модель, ему надо вместо реального класса (или трейта) предоставить специально созданный для этой цели класс с нужными полями:
@ApiModel(value = "Permission")
case class FakePermission(
       @ApiModelProperty(
         name = "$type",
         allowableValues = "ru.myproject.shared.Read, ru.myproject.shared.Read"
       )
       t: String,
       @ApiModelProperty(allowableValues = "permission.drill, permission.team"
       obj: String
     )
Теперь мы должны указывать в аннотации FakePermission вместо Permission
@ApiImplicitParams(Array(
    new ApiImplicitParam(
      value = "Допуск",
      required = true,
      dataTypeClass = classOf[FakePermission],
      paramType = "body"
    )
  ))
Коллекции
Последнее, на что хотел обратить внимание читателей. Как я уже говорил, Swagger не понимает обобщенные типы. Однако работать с коллекциями он умеет.
Так, аннотация
@ApiOperation имеет параметр responseContainer, которому можно передать значение «List».Что касается входящих параметров, указание
dataType = "List[ru.myproject.shared.roles.FakePermission]"
внутри аннотаций, поддерживающих этот атрибут, приводит к желаемым результатам. Хотя, если указать scala.collection.List — не работает.
Вывод
В моем проекте при помощи аннотаций Swagger-Core удалось полностью описать Rest-API и все модели данных, включая обобщенные типы и алгебраические типы данных. На мой взгляд использования модуля Swagger-Play является оптимальным для автоматический генерации описания API.
 
          