Offenes API-Spiel: Swagger Play


In diesem Artikel möchte ich anhand von Beispielen aus der Praxis erläutern, wie das Swagger-Modul für das Play Framework verwendet wird. Ich werde es erzählen:

  1. So befestigen Sie die neueste Version von Swagger-Play (Play-Modul, mit dem Sie Swagger-API-Annotationen verwenden und Dokumentation basierend auf der OpenAPI-Spezifikation erstellen können) und wie konfigurieren Sie Swagger-UI (Javascript-Bibliothek zur Visualisierung der generierten Dokumentation)
  2. Ich werde die Hauptanmerkungen von Swagger-Core beschreiben und über die Funktionen ihrer Verwendung für Scala sprechen
  3. Ich werde Ihnen erklären, wie Sie mit Datenmodellklassen richtig arbeiten
  4. Wie man das generische Typproblem in Swagger umgeht, das nicht weiß, wie man mit Generika arbeitet
  5. Wie man Swagger beibringt, ADT (algebraische Datentypen) zu verstehen
  6. Wie man Sammlungen beschreibt

Der Artikel wird für alle interessant sein, die das Play Framework auf Scala verwenden und die Dokumentation der API automatisieren werden.

Abhängigkeit hinzufügen


Nachdem ich viele Quellen im Internet studiert habe, komme ich zu dem Schluss, dass Sie das Swagger Play2-Modul installieren müssen, um Freunde von Swagger und Play Framework zu finden.

Bibliotheksadresse auf github:

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

Abhängigkeit hinzufügen :

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

Und hier tritt das Problem auf:

Zum Zeitpunkt dieses Schreibens wurde die Abhängigkeit weder aus den Maven-Central- noch aus den Sonatype-Repositorys gezogen.

In Maven-Central endeten alle gefundenen Builds in Scala 2.12. Im Allgemeinen gab es für Scala 2.13 keine einzige zusammengesetzte Version.

Ich hoffe wirklich, dass sie in Zukunft erscheinen werden.

Beim Aufstieg in das Sonatype-Releases-Repository habe ich den aktuellen Zweig dieser Bibliothek gefunden. Adresse auf github:

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

Also fügen wir die Abhängigkeit ein:

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

Fügen Sie das Sonatype-Repository hinzu:

resolvers += Resolver.sonatypeRepo("releases")

(Nicht erforderlich, da diese Assembly Maven-zentral ist.)

Jetzt muss das Modul in der Konfigurationsdatei application.conf aktiviert werden

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

sowie eine Route zu Routen hinzufügen:

GET     /swagger.json           controllers.ApiHelpController.getResources

Und das Modul ist betriebsbereit.

Jetzt generiert das Swagger Play-Modul eine JSON-Datei, die in einem Browser angezeigt werden kann.

Um die Funktionen von Swagger voll nutzen zu können, müssen Sie auch die Visualisierungsbibliothek herunterladen: swagger-ui. Es bietet eine praktische grafische Oberfläche zum Lesen der Datei swagger.json sowie die Möglichkeit, Restanforderungen an den Server zu senden. Dies ist eine hervorragende Alternative zu Postman, Rest-Client und anderen ähnlichen Tools.

Fügen Sie also abhängig hinzu:

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

Im Controller erstellen wir eine Methode, die Aufrufe an die statische Bibliothek index.html umleitet:

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

Nun, wir schreiben die Route in der Routendatei vor:

GET   /docs                   controllers.HomeController.redirectDocs()

Natürlich müssen Sie die Webjars-Play-Bibliothek verbinden. Hinzufügen abhängig:

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

Und fügen Sie die Route zur Routendatei hinzu:

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

Vorausgesetzt, unsere Anwendung wird ausgeführt, geben wir den Browser

http: // localhost: 9000 / docs ein

. Wenn alles richtig gemacht wurde, gelangen wir zur Prahlerseite unserer Anwendung:



Die Seite enthält noch keine Daten zu unserer Rest-API. Um dies zu ändern, müssen Sie Anmerkungen verwenden, die vom Swagger-Play-Modul gescannt werden.

Anmerkungen


Eine detaillierte Beschreibung aller Swagger-API-Core-Annotationen finden Sie unter:

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

In meinem Projekt habe ich die folgenden Annotationen verwendet:

@Api - stellt die Controller-Klasse fest als Swagger Ressource (zum Abtasten)

@ApiImplicitParam - beschreibt ein „impliziten“ Parameter (beispielsweise in dem Anforderungskörper angegeben)

@ApiImplicitParams - dient als Behälter für mehr @ApiImplicitParam Annotationen

@ApiModel - ermöglichen Ihnen , die beschreiben

@ApiModelProperty Datenmodell - beschreiben und das interpretieren

@ApiOperation Datenmodellklassenfeld - beschreibt die Controller - Methode (wahrscheinlich die Hauptanmerkung auf dieser Liste)

@ApiParam- beschreibt den explizit angegebenen Anforderungsparameter (z. B. in der Abfragezeichenfolge )

@ApiResponse - beschreibt die Serverantwort auf die

@ApiResponses- Anforderung - dient als Container für mehrere @ApiResponse- Annotationen . Enthält normalerweise zusätzliche Antworten (z. B. wenn Fehlercodes auftreten). Eine erfolgreiche Antwort wird normalerweise in der @ApiOperation- Annotation beschrieben .

Damit Swagger die Controller-Klasse scannen kann, müssen Sie die @Api- Annotation hinzufügen

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

Dies reicht Swagger aus, um Routen in Bezug auf Controller-Methoden in der Routendatei zu finden und zu beschreiben.



Es reicht jedoch eindeutig nicht aus, nur die Swagger-Controller-Klasse anzugeben. Swagger wartet mit anderen Anmerkungen auf uns.

Warum kann Swagger das nicht automatisch tun? Weil er keine Ahnung hat, wie unsere Klassen serialisiert werden. In diesem Projekt benutze ich uPickle, jemand benutzt Circe, jemand Play-JSON. Daher müssen Sie Links zu den empfangenen und ausgegebenen Klassen bereitstellen.

Da die verwendete Bibliothek in Java geschrieben ist, enthält das Scala-Projekt viele Nuancen.

Und das erste, was Sie tun müssen, ist die Syntax: Verschachtelte Anmerkungen funktionieren nicht.

Beispiel: Java-Code:

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


In Scala wird es so aussehen:

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


Beispiel 1


Beschreiben wir also eine Controller-Methode, die nach einer Entität in einer Datenbank sucht:

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


Mithilfe von Anmerkungen können wir eine Beschreibung der Methode, einen aus der Abfragezeichenfolge erhaltenen Eingabeparameter sowie Antworten vom Server angeben. Bei Erfolg gibt die Methode eine Instanz der Drill-Klasse zurück:

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




Wir haben eine gute Beschreibung. Swagger ahnte fast, womit das Objekt serialisiert wurde, mit einer Ausnahme: Die Start- und Endfelder in unserer Drill-Klasse sind Sofortobjekte und werden in Long serialisiert. Ich möchte 0 durch geeignetere Werte ersetzen. Wir können dies tun, indem wir die Annotationen @ApiModel, @ApiModelProperty auf unsere Klasse anwenden:

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


Jetzt haben wir eine absolut korrekte Beschreibung des Modells:




Beispiel 2


Zur Beschreibung der Post-Methode, bei der der Eingabeparameter im Anforderungshauptteil übergeben wird, wird die Annotation @ApiImplicitParams verwendet:

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

Beispiel 3


Bisher war alles einfach. Hier ist ein komplexeres Beispiel. Angenommen, es gibt eine verallgemeinerte Klasse in Abhängigkeit von einem Typparameter:

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

Zumindest versteht Swagger Generika noch nicht. Wir können in der Anmerkung nicht angeben:

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


In dieser Situation besteht die einzige Möglichkeit darin, für jeden der benötigten Typen einen generischen Typ zu unterordnen. Zum Beispiel könnten wir DrillSessionedResponse unterordnen.
Das einzige Problem ist, dass wir nicht von der Fallklasse erben können. Glücklicherweise hindert mich in meinem Projekt nichts daran, die Fallklasse in Klasse zu ändern. Dann:

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)

Jetzt kann ich diese Klasse in der Anmerkung angeben:

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

Beispiel 4


Jetzt ein noch komplexeres Beispiel für ADT - algebraische Datentypen.

Der Swagger bietet einen Mechanismus für die Arbeit mit ADT:

Zusammenfassung @ApiModel bietet zu diesem Zweck zwei Optionen:

1. Untertypen - Aufzählung von Unterklassen

2. DISCRIMINATOR - Feld, in dem sich Unterklassen voneinander unterscheiden.

In meinem Fall fügt uPickle, das JSON aus Fallklassen erzeugt, das Feld $ type selbst hinzu und case - serialisiert Objekte in Zeichenfolgen. Der Ansatz mit dem Diskriminatorfeld war also nicht akzeptabel.

Ich habe einen anderen Ansatz gewählt. Nehmen wir an, es gibt

sealed trait Permission

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


Dabei ist Obj ein weiteres ADT, das aus Fallobjekten besteht:

//  permission.drill
case object DrillObj extends Obj

// permission.team
case object TeamObj extends Obj


Damit Swagger dieses Modell anstelle einer realen Klasse (oder Eigenschaft) verstehen kann, muss es eine speziell für diesen Zweck erstellte Klasse mit den erforderlichen Feldern versehen:

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

Jetzt müssen wir FakePermission in der Annotation anstelle von Permission angeben

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

Sammlungen


Das Letzte, worauf ich die Aufmerksamkeit der Leser lenken wollte. Wie gesagt, Swagger versteht keine generischen Typen. Er weiß jedoch, wie man mit Sammlungen arbeitet.

Die @ApiOperation- Annotation verfügt also über einen responseContainer-Parameter, an den Sie den Wert "List" übergeben können.

In Bezug auf die Eingabeparameter eine Anzeige

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

Innerhalb von Anmerkungen, die dieses Attribut unterstützen, werden die gewünschten Ergebnisse erzielt. Wenn Sie jedoch scala.collection.List angeben, funktioniert dies nicht.

Fazit


In meinem Projekt konnte ich mithilfe der Swagger-Core-Annotationen die Rest-API und alle Datenmodelle, einschließlich generischer Typen und algebraischer Datentypen, vollständig beschreiben. Meiner Meinung nach ist die Verwendung des Swagger-Play-Moduls optimal, um automatisch API-Beschreibungen zu generieren.

All Articles