Игра с открытым 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.

ссылка на оригинал статьи https://habr.com/ru/post/503958/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *