Jogo API aberto: jogo Swagger


Neste artigo, quero dizer como usar o módulo Swagger para o Play Framework, com exemplos da vida real. Vou dizer:

  1. 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)
  2. Descreverei as principais anotações do Swagger-Core e falarei sobre os recursos de seu uso no Scala
  3. Vou lhe dizer como trabalhar com as classes de modelo de dados corretamente
  4. Como contornar o problema de tipo genérico no Swagger, que não sabe trabalhar com genéricos
  5. Como ensinar o Swagger a entender o ADT (tipos de dados algébricos)
  6. 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-play

Adicione 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-play

Entã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.conf

play.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/*file               controllers.Assets.at(path="/public", file)

Desde que nosso aplicativo esteja em execução, digite o navegador

http: // localhost: 9000 / docs

e, 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.X

No 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.Por

exemplo, 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 subclasses

2. 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 exista

sealed 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:

//  permission.drill
case object DrillObj extends Obj

// permission.team
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ção

dataType = "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.

All Articles