Juego de API abierta: Swagger Play


En este artículo quiero decir cómo usar el módulo Swagger para Play Framework, con ejemplos de la vida real. Diré:

  1. Cómo fijar la última versión de Swagger-Play (módulo Play que le permite usar anotaciones swagger-api y generar documentación basada en la especificación OpenAPI) y cómo configurar swagger-ui (biblioteca javascript utilizada para visualizar la documentación generada)
  2. Describiré las principales anotaciones de Swagger-Core y hablaré sobre las características de su uso para Scala
  3. Te diré cómo trabajar correctamente con las clases de modelo de datos.
  4. Cómo solucionar el problema del tipo genérico en Swagger, que no sabe cómo trabajar con genéricos
  5. Cómo enseñar a Swagger a comprender ADT (tipos de datos algebraicos)
  6. Cómo describir colecciones

El artículo será interesante para todos los que usan Play Framework en Scala y van a automatizar la documentación de la API.

Agregar dependencia


Habiendo estudiado muchas fuentes en Internet, concluyo que para hacer amigos de Swagger y Play Framework, debes instalar el módulo Swagger Play2.

Dirección de la biblioteca en github:

https://github.com/swagger-api/swagger-play

Agregar dependencia:

libraryDependencies ++= Seq(
  "io.swagger" %% "swagger-play2" % "2.0.1-SNAPSHOT"
)

Y aquí surge el problema:

en el momento de escribir esto, la dependencia no se extraía de los repositorios Maven-central o Sonatype.

En Maven-central, todas las construcciones encontradas terminaron en Scala 2.12. En general, no hubo una sola versión ensamblada para Scala 2.13.

Realmente espero que en el futuro aparezcan.

Al subir al repositorio de lanzamientos de Sonatype, encontré la bifurcación actual de esta biblioteca. Dirección en github:

https://github.com/iterable/swagger-play

Entonces, insertamos la dependencia:

libraryDependencies ++= Seq(
  "com.iterable" %% "swagger-play" % "2.0.1"
)

Agregue el repositorio de Sonatype:

resolvers += Resolver.sonatypeRepo("releases")

(No es necesario, ya que este ensamblaje es Maven-central)

Ahora queda activar el módulo en el archivo de configuración application.conf

play.modules.enabled += "play.modules.swagger.SwaggerModule"

así como agregar una ruta a las rutas:

GET     /swagger.json           controllers.ApiHelpController.getResources

Y el módulo está listo para funcionar.

Ahora el módulo Swagger Play generará un archivo json que se puede ver en un navegador.

Para disfrutar plenamente de las funciones de Swagger, también debe descargar la biblioteca de visualización: swagger-ui. Proporciona una interfaz gráfica conveniente para leer el archivo swagger.json, así como la capacidad de enviar solicitudes de descanso al servidor, proporcionando una excelente alternativa para Postman, Rest-client y otras herramientas similares.

Entonces, agregue dependiendo:

libraryDependencies += "org.webjars" % "swagger-ui" % "3.25.3"

En el controlador, creamos un método que redirige las llamadas al archivo de biblioteca estática index.html:

def redirectDocs: Action[AnyContent] = Action {
    Redirect(
       url = "/assets/lib/swagger-ui/index.html",
       queryStringParams = Map("url" -> Seq("/swagger.json")))
  }

Bueno, prescribimos la ruta en el archivo de rutas:

GET   /docs                   controllers.HomeController.redirectDocs()

Por supuesto, debe conectar la biblioteca webjars-play. Agregar según:

libraryDependencies +=  "org.webjars" %% "webjars-play" % "2.8.0"

Y agregue la ruta al archivo de rutas:

GET     /assets/*file               controllers.Assets.at(path="/public", file)

Siempre que nuestra aplicación se esté ejecutando, escribimos en el navegador

http: // localhost: 9000 / docs

y, si todo se hace correctamente, llegamos a la página de swagger de nuestra aplicación:



La página aún no contiene datos sobre nuestra API de descanso. Para cambiar esto, debe usar anotaciones, que serán escaneadas por el módulo Swagger-Play.

Anotaciones


Se puede encontrar una descripción detallada de todas las anotaciones swagger-api-core en:

https://github.com/swagger-api/swagger-core/wiki/Annotations-1.5.X

En mi proyecto utilicé las siguientes anotaciones:

@Api - anota la clase de controlador como un recurso Swagger (para el escaneado)

@ApiImplicitParam - describe un parámetro "implícita" (por ejemplo, especificado en el cuerpo de la solicitud)

@ApiImplicitParams - sirve como un contenedor para varios @ApiImplicitParam anotaciones

@ApiModel - le permite describir la

@ApiModelProperty datos modelo - describe e interpreta la

@ApiOperation campo de clase de modelo de datos - se describe el método controlador (probablemente la anotación principal en esta lista)

@ApiParam- describe el parámetro de solicitud especificado explícitamente (en la cadena de consulta, por ejemplo)

@ApiResponse - describe la respuesta del servidor a la solicitud de

@ApiResponses - sirve como contenedor para varias anotaciones de @ApiResponse. Por lo general, incluye respuestas adicionales (por ejemplo, cuando se producen códigos de error). Por lo general, una respuesta exitosa se describe en la anotación @ApiOperation.

Entonces, para que Swagger escanee la clase de controlador, debe agregar la anotación @Api

@Api(value = «RestController», produces = «application/json»)
class RestController @Inject()(

Esto es suficiente para que Swagger encuentre rutas relacionadas con los métodos del controlador en el archivo de rutas e intente describirlas.



Pero simplemente especificar la clase de controlador Swagger claramente no es suficiente. Swagger nos está esperando con otras anotaciones.

¿Por qué Swagger no puede hacer esto automáticamente? Porque no tiene idea de cómo se serializan nuestras clases. En este proyecto uso uPickle, alguien usa Circe, alguien Play-JSON. Por lo tanto, debe proporcionar enlaces a las clases recibidas y emitidas.

Como la biblioteca utilizada está escrita en Java, hay muchos matices en el proyecto Scala.

Y lo primero con lo que tiene que lidiar es la sintaxis: las anotaciones anidadas no funcionan.

Por ejemplo, el código Java:

@ApiResponses(value = {
      @ApiResponse(code = 400, message = "Invalid ID supplied"),
      @ApiResponse(code = 404, message = "Pet not found") })


En Scala se verá así:

@ApiResponses(value = Array(
      new ApiResponse(code = 400, message = "Invalid ID supplied"),
      new ApiResponse(code = 404, message = "Pet not found") ))


Ejemplo 1


Entonces, describamos un método de controlador que busca una entidad en una base de datos:

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)
    }      


Mediante anotaciones, podemos especificar una descripción del método, un parámetro de entrada obtenido de la cadena de consulta y también respuestas del servidor. Si tiene éxito, el método devolverá una instancia de la clase 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)
    }




Tenemos una buena descripción. Swagger casi adivinó con qué se serializó el objeto, con una excepción: los campos de inicio y finalización en nuestra clase Drill son objetos instantáneos y se serializan en Long. Me gustaría reemplazar 0 con valores más adecuados. Podemos hacer esto aplicando las anotaciones @ApiModel, @ApiModelProperty a nuestra clase:

@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
                )


Ahora tenemos una descripción absolutamente correcta del modelo:




Ejemplo 2


Para describir el método Post, donde el parámetro de entrada se pasa en el cuerpo de la solicitud, se @usa la anotación 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 =>

Ejemplo 3


Hasta ahora, todo era simple. Aquí hay un ejemplo más complejo. Supongamos que hay una clase generalizada según un parámetro de tipo:

case class SessionedResponse[T](
                            val ses: SessionData,
                            val payload: T
                          )

Swagger todavía no entiende los genéricos, al menos. No podemos indicar en la anotación:

@ApiOperation(
    value = " ",
    response = classOf[SessionedResponse[Drill]]
  )


La única forma en esta situación es subclasificar un tipo genérico para cada uno de los tipos que necesitamos. Por ejemplo, podríamos subclasificar DrillSessionedResponse.
El único problema es que no podemos heredar de la clase de caso. Afortunadamente, en mi proyecto, nada me impide cambiar la clase de caso a clase. Entonces:

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)

Ahora puedo especificar esta clase en la anotación:

@ApiOperation(
    value = " ",
    response = classOf[DrillSessionedResponse]
  )

Ejemplo 4


Ahora un ejemplo aún más complejo relacionado con ADT - tipos de datos algebraicos.

Swagger proporciona un mecanismo para trabajar con ADT:

Abstract @ApiModel tiene 2 opciones para este propósito:

1. subTypes - enumeración de subclases

2. DISCRIMINATOR - campo en el que las subclases difieren entre sí.

En mi caso, uPickle, al producir JSON a partir de las clases de casos, agrega el campo $ type en sí y serializa los objetos en cadenas. Por lo tanto, el enfoque con el campo discriminador no era aceptable.

Usé un enfoque diferente. Digamos que hay

sealed trait Permission

case class Write(obj: Obj) extends Permission
case class Read(obj: Obj) extends Permission


donde Obj es otro ADT que consiste en objetos de caso:

//  permission.drill
case object DrillObj extends Obj

// permission.team
case object TeamObj extends Obj


Para que Swagger entienda este modelo, en lugar de una clase real (o rasgo), necesita proporcionar una clase especialmente creada para este propósito con los campos necesarios:

@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
     )

Ahora debemos especificar FakePermission en la anotación en lugar de Permission

@ApiImplicitParams(Array(
    new ApiImplicitParam(
      value = "",
      required = true,
      dataTypeClass = classOf[FakePermission],
      paramType = "body"
    )
  ))

Colecciones


Lo último que quería llamar la atención de los lectores. Como dije, Swagger no entiende los tipos genéricos. Sin embargo, él sabe trabajar con colecciones.

Por lo tanto, la anotación @ApiOperation tiene un parámetro responseContainer al que puede pasar el valor "Lista".

En cuanto a los parámetros de entrada, una indicación

dataType = "List[ru.myproject.shared.roles.FakePermission]"

dentro de las anotaciones que admiten este atributo, produce los resultados deseados. Aunque, si especifica scala.collection.List, no funciona.

Conclusión


En mi proyecto, usando las anotaciones Swagger-Core, pude describir completamente la API Rest y todos los modelos de datos, incluidos los tipos genéricos y los tipos de datos algebraicos. En mi opinión, usar el módulo Swagger-Play es óptimo para generar automáticamente descripciones de API.

All Articles