Neste artigo, quero dizer como usar o módulo Swagger para o Play Framework, com exemplos da vida real. Vou dizer:- Como fixar a versão mais recente do Swagger-Play (módulo Play que permite usar anotações swagger-api e gerar documentação com base na especificação OpenAPI) e como configurar o swagger-ui (biblioteca javascript usada para visualizar a documentação gerada)
- Descreverei as principais anotações do Swagger-Core e falarei sobre os recursos de seu uso no Scala
- Vou lhe dizer como trabalhar com as classes de modelo de dados corretamente
- Como contornar o problema de tipo genérico no Swagger, que não sabe trabalhar com genéricos
- Como ensinar o Swagger a entender o ADT (tipos de dados algébricos)
- Como descrever coleções
O artigo será interessante para todos que usam o Play Framework no Scala e automatizará a documentação da API.Adicionar dependência
Tendo estudado muitas fontes na Internet, concluo que, para fazer amigos do Swagger e do Play Framework, você precisa instalar o módulo Swagger Play2.Endereço da biblioteca no github:https://github.com/swagger-api/swagger-playAdicione dependência:libraryDependencies ++= Seq(
"io.swagger" %% "swagger-play2" % "2.0.1-SNAPSHOT"
)
E aqui surge o problema:No momento da redação deste artigo, a dependência não estava sendo extraída dos repositórios Maven-central ou Sonatype.No Maven-central, todas as compilações encontradas terminaram no Scala 2.12. Em geral, não havia uma única versão montada para o Scala 2.13.Eu realmente espero que eles apareçam no futuro.Subindo no repositório de versões do Sonatype, encontrei o fork atual desta biblioteca. Endereço no github:https://github.com/iterable/swagger-playEntão, inserimos a dependência:libraryDependencies ++= Seq(
"com.iterable" %% "swagger-play" % "2.0.1"
)
Adicione o repositório Sonatype:resolvers += Resolver.sonatypeRepo("releases")
(Não é necessário, pois este assembly é central para o Maven)Agora resta ativar o módulo no arquivo de configuração application.confplay.modules.enabled += "play.modules.swagger.SwaggerModule"
bem como adicionar uma rota às rotas:GET /swagger.json controllers.ApiHelpController.getResources
E o módulo está pronto para começar.Agora, o módulo Swagger Play irá gerar um arquivo json que pode ser visualizado em um navegador.Para aproveitar totalmente os recursos do Swagger, você também precisa fazer o download da biblioteca de visualização: swagger-ui. Ele fornece uma interface gráfica conveniente para a leitura do arquivo swagger.json, bem como a capacidade de enviar solicitações de descanso para o servidor, fornecendo uma excelente alternativa ao Postman, Rest-client e outras ferramentas similares.Então, adicione dependendo:libraryDependencies += "org.webjars" % "swagger-ui" % "3.25.3"
No controlador, criamos um método que redireciona as chamadas para o arquivo index.html da biblioteca estática:def redirectDocs: Action[AnyContent] = Action {
Redirect(
url = "/assets/lib/swagger-ui/index.html",
queryStringParams = Map("url" -> Seq("/swagger.json")))
}
Bem, prescrevemos a rota no arquivo de rotas:GET /docs controllers.HomeController.redirectDocs()
Obviamente, você precisa conectar a biblioteca webjars-play. Adicionar dependendo:libraryDependencies += "org.webjars" %% "webjars-play" % "2.8.0"
E adicione a rota ao arquivo de rotas:GET /assets
Desde que nosso aplicativo esteja em execução, digite o navegadorhttp: // localhost: 9000 / docse, se tudo for feito corretamente, chegaremos à página de arrogância de nosso aplicativo:A página ainda não contém dados sobre a nossa rest-api. Para alterar isso, você precisa usar anotações, que serão digitalizadas pelo módulo Swagger-Play.Anotações
Uma descrição detalhada de todas as anotações do swagger-api-core pode ser encontrada em:https://github.com/swagger-api/swagger-core/wiki/Annotations-1.5.XNo meu projeto, usei as seguintes anotações:@
Api - observa a classe do controlador como um recurso Swagger (para digitalização)@
ApiImplicitParam - descreve um parâmetro "implícito" (por exemplo, especificado no corpo da solicitação)@
ApiImplicitParams - serve como um recipiente para vários @
ApiImplicitParam anotações@
ApiModel - permite descrever o@
ApiModelProperty dados modelo - descreve e interpreta o@
ApiOperation campo de classe modelo de dados - descreve o método de controlador (provavelmente a anotação principal nesta lista)@
ApiParam- descreve o parâmetro de solicitação especificado explicitamente (na sequência de consultas, por exemplo)@
ApiResponse - descreve a resposta do servidor à solicitação@
ApiResponses - serve como um contêiner para várias anotações do @
ApiResponse. Geralmente inclui respostas adicionais (por exemplo, quando códigos de erro ocorrem). Uma resposta bem-sucedida é geralmente descrita na anotação @
ApiOperation.Portanto, para o Swagger varrer a classe do controlador, é necessário adicionar a anotação @
Api@Api(value = «RestController», produces = «application/json»)
class RestController @Inject()(
Isso é suficiente para o Swagger encontrar rotas relacionadas aos métodos do controlador no arquivo de rotas e tentar descrevê-las.Mas simplesmente especificar a classe do controlador Swagger claramente não é suficiente. Swagger está esperando por nós com outras anotações.Por que o Swagger não pode fazer isso automaticamente? Porque ele não tem ideia de como nossas aulas são serializadas. Neste projeto eu uso o uPickle, alguém usa Circe, alguém Play-JSON. Portanto, você deve fornecer links para as classes recebidas e emitidas.Como a biblioteca usada é escrita em Java, há muitas nuances no projeto Scala.E a primeira coisa com a qual você precisa lidar é com a sintaxe: anotações aninhadas não funcionam.Porexemplo, código Java:@ApiResponses(value = {
@ApiResponse(code = 400, message = "Invalid ID supplied"),
@ApiResponse(code = 404, message = "Pet not found") })
Em Scala, será assim:@ApiResponses(value = Array(
new ApiResponse(code = 400, message = "Invalid ID supplied"),
new ApiResponse(code = 404, message = "Pet not found") ))
Exemplo 1
Então, vamos descrever um método de controlador que procura uma entidade em um banco de dados: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)
}
Usando anotações, podemos especificar uma descrição do método, um parâmetro de entrada obtido da string de consulta e também respostas do servidor. Se for bem-sucedido, o método retornará uma instância da classe 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)
}
Temos uma boa descrição. Swagger quase adivinhou com o que o objeto foi serializado, com uma exceção: os campos de início e fim da classe Drill são objetos instantâneos e serializados em Long. Gostaria de substituir 0 por valores mais adequados. Podemos fazer isso aplicando as anotações @ApiModel, @ApiModelProperty à nossa classe:@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
)
Agora, temos uma descrição absolutamente correta do modelo:
Exemplo 2
Para descrever o método Post, em que o parâmetro de entrada é passado no corpo da solicitação, a anotação @
ApiImplicitParams é usada: @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 =>
Exemplo 3
Até agora, tudo era simples. Aqui está um exemplo mais complexo. Suponha que exista uma classe generalizada, dependendo de um parâmetro de tipo:case class SessionedResponse[T](
val ses: SessionData,
val payload: T
)
O Swagger ainda não entende os genéricos, pelo menos. Não podemos indicar na anotação:@ApiOperation(
value = " ",
response = classOf[SessionedResponse[Drill]]
)
A única maneira nessa situação é subclassificar um tipo genérico para cada um dos tipos que precisamos. Por exemplo, poderíamos subclassificar DrillSessionedResponse.O único problema é que não podemos herdar da classe de caso. Felizmente, no meu projeto, nada me impede de mudar a classe de caso para outra. Então: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)
Agora eu posso especificar esta classe na anotação:@ApiOperation(
value = " ",
response = classOf[DrillSessionedResponse]
)
Exemplo 4
Agora, um exemplo ainda mais complexo está relacionado aos tipos de dados algébricos do ADT.O Swagger fornece um mecanismo para trabalhar com o ADT:Resumo O @
ApiModel possui 2 opções para esse fim:1. subTypes - enumeração de subclasses2. DISCRIMINATOR - campo no qual as subclasses diferem umas das outras.No meu caso, o uPickle, produzindo JSON a partir de classes de casos, adiciona o próprio campo $ type e serializa objetos em seqüências de caracteres. Portanto, a abordagem com o campo discriminador não era aceitável.Eu usei uma abordagem diferente. Digamos que existasealed trait Permission
case class Write(obj: Obj) extends Permission
case class Read(obj: Obj) extends Permission
onde Obj é outro ADT que consiste em objetos de caso:
case object DrillObj extends Obj
case object TeamObj extends Obj
Para que o Swagger entenda esse modelo, em vez de uma classe (ou característica) real, ele precisa fornecer uma classe criada especialmente para esse fim com os campos necessários:@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
)
Agora devemos especificar FakePermission na anotação em vez de Permission@ApiImplicitParams(Array(
new ApiImplicitParam(
value = "",
required = true,
dataTypeClass = classOf[FakePermission],
paramType = "body"
)
))
Colecções
A última coisa que eu queria chamar a atenção dos leitores. Como eu disse, o Swagger não entende tipos genéricos. No entanto, ele sabe como trabalhar com coleções.Portanto, a anotação @
ApiOperation possui um parâmetro responseContainer para o qual você pode passar o valor "List".Em relação aos parâmetros de entrada, uma indicaçãodataType = "List[ru.myproject.shared.roles.FakePermission]"
nas anotações que suportam esse atributo, produz os resultados desejados. Embora, se você especificar scala.collection.List - ele não funcionará.Conclusão
No meu projeto, usando as anotações do Swagger-Core, consegui descrever completamente a API Rest e todos os modelos de dados, incluindo tipos genéricos e tipos de dados algébricos. Na minha opinião, o uso do módulo Swagger-Play é ideal para gerar automaticamente descrições de API.