В данной статье я хочу рассказать, как использовать 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)
При условии, что наше приложение запущено, набираем в браузере
и, если все сделано правильно, попадаем на страницу 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/
Добавить комментарий