В данной статье я хочу рассказать, как использовать 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.