Игра с открытым API: Swagger Play


В данной статье я хочу рассказать, как использовать Swagger модуль для Play Framework, с примерами из реальной жизни. Я расскажу:

  1. Как прикрутить последнюю версию Swagger-Play (модуль Play, позволяющий использовать аннотации swagger-api и генерировать на их основе документацию в соответствии со спецификацией OpenAPI) и как настроить swagger-ui (библиотеку javascript, служащую для визуализации сгенерированной документации)
  2. Опишу основные аннотации Swagger-Core и расскажу об особенностях их использования для Scala
  3. Расскажу, как правильно работать с классами моделей данных
  4. Как обойти проблему обобщенных типов в Swagger, который не умеет работать с дженериками
  5. Как научить Swagger понимать ADT (алгебраические типы данных)
  6. Как описывать коллекции

Статья будет интересна всем, кто использует 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.
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 0

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое