Jeu API ouvert: Swagger Play


Dans cet article, je veux expliquer comment utiliser le module Swagger pour Play Framework, avec des exemples concrets. Je dirai:

  1. Comment fixer la dernière version de Swagger-Play (module Play qui vous permet d'utiliser des annotations swagger-api et de générer de la documentation basée sur la spécification OpenAPI) et comment configurer swagger-ui (bibliothèque javascript utilisée pour visualiser la documentation générée)
  2. Je vais décrire les principales annotations de Swagger-Core et parler des caractéristiques de leur utilisation pour Scala
  3. Je vais vous expliquer comment travailler correctement avec des classes de modèle de données
  4. Comment contourner le problème de type générique dans Swagger, qui ne sait pas comment travailler avec des génériques
  5. Comment apprendre à Swagger à comprendre l'ADT (types de données algébriques)
  6. Comment décrire les collections

L'article sera intéressant pour tous ceux qui utilisent Play Framework sur Scala et va automatiser la documentation de l'API.

Ajouter une dépendance


Après avoir étudié de nombreuses sources sur Internet, je conclus que pour se faire des amis Swagger et Play Framework, vous devez installer le module Swagger Play2.

Adresse de la bibliothèque sur github:

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

Ajouter une dépendance:

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

Et ici, le problème se pose:

au moment d'écrire ces lignes, la dépendance n'était pas extraite des référentiels Maven-central ou Sonatype.

Dans Maven-central, toutes les versions trouvées se sont terminées dans Scala 2.12. En général, il n'y avait pas une seule version assemblée pour Scala 2.13.

J'espère vraiment qu'ils apparaîtront à l'avenir.

En escaladant le référentiel des versions de Sonatype, j'ai trouvé le fork actuel de cette bibliothèque. Adresse sur github:

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

Donc, nous insérons la dépendance:

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

Ajoutez le référentiel Sonatype:

resolvers += Resolver.sonatypeRepo("releases")

(Pas nécessaire, car cet assemblage est Maven-central)

Il reste maintenant à activer le module dans le fichier de configuration application.conf

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

ainsi que d'ajouter un itinéraire aux itinéraires:

GET     /swagger.json           controllers.ApiHelpController.getResources

Et le module est prêt à fonctionner.

Le module Swagger Play va maintenant générer un fichier json qui peut être visualisé dans un navigateur.

Pour profiter pleinement des fonctionnalités de Swagger, vous devez également télécharger la bibliothèque de visualisation: swagger-ui. Il fournit une interface graphique pratique pour lire le fichier swagger.json, ainsi que la possibilité d'envoyer des demandes de repos au serveur, offrant une excellente alternative à Postman, Rest-client et à d'autres outils similaires.

Alors, ajoutez en fonction:

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

Dans le contrôleur, nous créons une méthode qui redirige les appels vers le fichier de bibliothèque statique index.html:

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

Eh bien, nous prescrivons l'itinéraire dans le fichier itinéraires:

GET   /docs                   controllers.HomeController.redirectDocs()

Bien sûr, vous devez connecter la bibliothèque webjars-play. Ajouter en fonction:

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

Et ajoutez l'itinéraire au fichier routes:

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

À condition que notre application soit en cours d'exécution, nous tapons dans le navigateur

http: // localhost: 9000 / docs

et, si tout est fait correctement, nous arrivons à la page swagger de notre application:



La page ne contient pas encore de données sur nos rest-api. Pour changer cela, vous devez utiliser des annotations, qui seront analysées par le module Swagger-Play.

Annotations


Une description détaillée de toutes les annotations swagger-api-core peut être trouvée sur:

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

Dans mon projet, j'ai utilisé les annotations suivantes:

@Api - note la classe de contrôleur comme une ressource Swagger (par balayage)

@ApiImplicitParam - décrit un paramètre « implicite » (par exemple, spécifié dans le corps de la demande)

@ApiImplicitParams - sert de conteneur pour plusieurs @ApiImplicitParam annotations

@ApiModel - permet de décrire les

@ApiModelProperty données modèle - décrit et interprète le

@ApiOperation champ de classe de modèle de données - décrit la méthode de commande (probablement l'annotation principale de cette liste)

@ApiParam- décrit le paramètre de requête spécifié explicitement (dans la chaîne de requête, par exemple)

@ApiResponse - décrit la réponse du serveur à la requête

@ApiResponses - sert de conteneur pour plusieurs annotations @ApiResponse. Comprend généralement des réponses supplémentaires (par exemple, lorsque des codes d'erreur se produisent). Une réponse réussie est généralement décrite dans l'annotation @ApiOperation.

Ainsi, pour que Swagger analyse la classe de contrôleur, vous devez ajouter l'annotation @Api

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

Cela suffit à Swagger pour trouver des routes liées aux méthodes de contrôleur dans le fichier routes et essayer de les décrire.



Mais simplement spécifier la classe de contrôleur Swagger n'est clairement pas suffisant. Swagger nous attend avec d'autres annotations.

Pourquoi Swagger ne peut-il pas le faire automatiquement? Parce qu'il n'a aucune idée de la façon dont nos classes sont sérialisées. Dans ce projet, j'utilise uPickle, quelqu'un utilise Circe, quelqu'un Play-JSON. Par conséquent, vous devez fournir des liens vers les classes reçues et émises.

La bibliothèque utilisée étant écrite en Java, le projet Scala comporte de nombreuses nuances.

Et la première chose à laquelle vous devez faire face est la syntaxe: les annotations imbriquées ne fonctionnent pas.

Par exemple, le code Java:

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


À Scala, cela ressemblera à ceci:

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


Exemple 1


Décrivons donc une méthode de contrôleur qui recherche une entité dans une base de données:

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


À l'aide d'annotations, nous pouvons spécifier une description de la méthode, un paramètre d'entrée obtenu à partir de la chaîne de requête, ainsi que des réponses du serveur. En cas de succès, la méthode renvoie une instance de la 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)
    }




Nous avons eu une bonne description. Swagger a presque deviné avec quoi l'objet était sérialisé, à une exception près: les champs de début et de fin de notre classe Drill sont des objets Instant, et sérialisés en Long. Je voudrais remplacer 0 par des valeurs plus appropriées. Nous pouvons le faire en appliquant les annotations @ApiModel, @ApiModelProperty à notre 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
                )


Maintenant, nous avons une description absolument correcte du modèle:




Exemple 2


Pour décrire la méthode Post, où le paramètre d'entrée est passé dans le corps de la demande, l'annotation @ApiImplicitParams est utilisée:

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

Exemple 3


Jusqu'à présent, tout était simple. Voici un exemple plus complexe. Supposons qu'il existe une classe généralisée en fonction d'un paramètre de type:

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

Swagger ne comprend pas encore les génériques, du moins. Nous ne pouvons pas indiquer dans l'annotation:

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


La seule façon dans cette situation est de sous-classer un type générique pour chacun des types dont nous avons besoin. Par exemple, nous pourrions sous-classer DrillSessionedResponse.
Le seul problème est que nous ne pouvons pas hériter de la classe de cas. Heureusement, dans mon projet, rien ne m'empêche de changer la classe de cas en classe. Alors:

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)

Maintenant, je peux spécifier cette classe dans l'annotation:

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

Exemple 4


Maintenant, un exemple encore plus complexe lié aux types de données ADT - algébriques.

Le Swagger fournit un mécanisme pour travailler avec ADT:

Abstract @ApiModel a 2 options à cet effet:

1. subTypes - énumération des sous-classes

2. DISCRIMINATOR - champ sur lequel les sous-classes diffèrent les unes des autres.

Dans mon cas, uPickle, produisant du JSON à partir des classes de cas, ajoute le champ $ type lui-même, et sérialise les objets en chaînes. L'approche avec le champ discriminateur n'était donc pas acceptable.

J'ai utilisé une approche différente. Disons qu'il y a

sealed trait Permission

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


où Obj est un autre ADT composé d'objets de cas:

//  permission.drill
case object DrillObj extends Obj

// permission.team
case object TeamObj extends Obj


Pour que Swagger comprenne ce modèle, au lieu d'une vraie classe (ou trait), il doit fournir une classe spécialement créée à cet effet avec les champs nécessaires:

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

Maintenant, nous devons spécifier FakePermission dans l'annotation au lieu de Permission

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

Les collections


La dernière chose que je voulais attirer l'attention des lecteurs. Comme je l'ai dit, Swagger ne comprend pas les types génériques. Cependant, il sait comment travailler avec des collections.

Ainsi, l'annotation @ApiOperation a un paramètre responseContainer auquel vous pouvez passer la valeur «List».

Concernant les paramètres d'entrée, une indication

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

dans les annotations qui prennent en charge cet attribut, produit les résultats souhaités. Cependant, si vous spécifiez scala.collection.List - cela ne fonctionne pas.

Conclusion


Dans mon projet, en utilisant les annotations Swagger-Core, j'ai pu décrire entièrement l'API Rest et tous les modèles de données, y compris les types génériques et les types de données algébriques. À mon avis, l'utilisation du module Swagger-Play est optimale pour générer automatiquement des descriptions d'API.

All Articles