Open API Game: Swagger Play


Pada artikel ini saya ingin memberi tahu cara menggunakan modul Swagger untuk Play Framework, dengan contoh nyata. Saya akan mengatakan:

  1. Cara mempercepat versi terbaru Swagger-Play (modul Play yang memungkinkan Anda untuk menggunakan anotasi swagger-api dan menghasilkan dokumentasi berdasarkan spesifikasi OpenAPI) dan cara mengonfigurasi swagger-ui (perpustakaan javascript yang digunakan untuk memvisualisasikan dokumentasi yang dihasilkan)
  2. Saya akan menjelaskan anotasi utama dari Swagger-Core dan berbicara tentang fitur penggunaannya untuk Scala
  3. Saya akan memberi tahu Anda cara bekerja dengan kelas model data dengan benar
  4. Bagaimana menyiasati masalah tipe generik di Swagger, yang tidak tahu cara bekerja dengan obat generik
  5. Cara mengajar Swagger untuk memahami ADT (tipe data aljabar)
  6. Cara mendeskripsikan koleksi

Artikel ini akan menarik bagi semua orang yang menggunakan Kerangka Bermain di Scala dan akan mengotomatisasi dokumentasi API.

Tambahkan ketergantungan


Setelah mempelajari banyak sumber di Internet, saya menyimpulkan bahwa untuk membuat teman-teman Swagger dan Play Framework, Anda perlu menginstal modul Swagger Play2.

Alamat perpustakaan di github:

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

Tambahkan ketergantungan:

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

Dan di sini muncul masalah:

Pada saat penulisan ini, ketergantungan tidak ditarik dari repositori Maven-pusat atau Sonatype.

Di Maven-pusat, semua bangunan yang ditemukan berakhir di Scala 2.12. Secara umum, tidak ada versi rakitan tunggal untuk Scala 2.13.

Saya sangat berharap bahwa di masa depan mereka akan muncul.

Memanjat repositori Sonatype-rilis, saya menemukan garpu saat ini dari perpustakaan ini. Alamat di github:

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

Jadi, kami menyisipkan dependensi:

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

Tambahkan repositori Sonatype:

resolvers += Resolver.sonatypeRepo("releases")

(Tidak perlu, karena rakitan ini Maven-tengah)

Sekarang tetap mengaktifkan modul dalam file konfigurasi application.conf

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

serta menambahkan rute ke rute:

GET     /swagger.json           controllers.ApiHelpController.getResources

Dan modul siap untuk digunakan.

Sekarang modul Swagger Play akan menghasilkan file json yang dapat dilihat di browser.

Untuk sepenuhnya menikmati fitur Swagger, Anda juga perlu mengunduh perpustakaan visualisasi: swagger-ui. Ini menyediakan antarmuka grafis yang nyaman untuk membaca file swagger.json, serta kemampuan untuk mengirim permintaan sisanya ke server, memberikan alternatif yang sangat baik untuk Postman, Rest-client, dan alat serupa lainnya.

Jadi, tambahkan tergantung:

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

Di controller, kami membuat metode yang mengalihkan panggilan ke file index.html perpustakaan statis:

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

Nah, kami meresepkan rute dalam file rute:

GET   /docs                   controllers.HomeController.redirectDocs()

Tentu saja, Anda harus menghubungkan pustaka webjars-play. Tambahkan tergantung:

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

Dan tambahkan rute ke file rute:

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

Asalkan aplikasi kami berjalan, kami mengetikkan browser

http: // localhost: 9000 / docs

dan, jika semuanya dilakukan dengan benar, kami membuka halaman angkuh aplikasi kami:



Halaman ini belum berisi data tentang rest-api kami. Untuk mengubah ini, Anda perlu menggunakan anotasi, yang akan dipindai oleh modul Swagger-Play.

Anotasi


Penjelasan terperinci dari semua anotasi swagger-api-core dapat ditemukan di:

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

Dalam proyek saya, saya menggunakan anotasi berikut:

@Api - mencatat kelas controller sebagai sumber daya Swagger (untuk pemindaian)

@ApiImplicitParam - menggambarkan "implisit" parameter (misalnya, ditentukan dalam tubuh permintaan)

@ApiImplicitParams - berfungsi sebagai wadah untuk beberapa @ApiImplicitParam penjelasan

@ApiModel - memungkinkan Anda untuk menggambarkan

@ApiModelProperty Data Model - menjelaskan dan menafsirkan yang

@ApiOperation bidang kelas model data - menggambarkan metode controller (mungkin anotasi utama dalam daftar ini)

@ApiParam- menjelaskan parameter permintaan yang ditentukan secara eksplisit (dalam string kueri, misalnya)

@ApiResponse - menjelaskan respons server terhadap permintaan

@ApiResponses - berfungsi sebagai wadah untuk beberapa anotasi @ApiResponse. Biasanya menyertakan jawaban tambahan (misalnya, ketika kode kesalahan terjadi). Respons yang berhasil biasanya dijelaskan dalam anotasi @ApiOperation.

Jadi, agar Swagger memindai kelas pengontrol, Anda perlu menambahkan anotasi @Api

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

Ini cukup bagi Swagger untuk menemukan rute yang terkait dengan metode pengontrol di file rute dan mencoba menggambarkannya.



Tetapi hanya menentukan kelas kontroler Swagger jelas tidak cukup. Kesombongan sedang menunggu kita dengan anotasi lainnya.

Mengapa Swagger tidak dapat melakukan ini secara otomatis? Karena dia tidak tahu bagaimana kelas kita diserialisasi. Dalam proyek ini saya menggunakan uPickle, seseorang menggunakan Circe, seseorang Play-JSON. Oleh karena itu, Anda harus memberikan tautan ke kelas yang diterima dan dikeluarkan.

Karena perpustakaan yang digunakan ditulis di Jawa, ada banyak nuansa dalam proyek Scala.

Dan hal pertama yang harus Anda hadapi adalah sintaksis: nested annotations tidak berfungsi

, misalnya kode Java:

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


Dalam Scala akan terlihat seperti ini:

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


Contoh 1


Jadi, mari kita gambarkan metode pengontrol yang mencari entitas dalam database:

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


Dengan menggunakan anotasi, kita dapat menentukan deskripsi metode, parameter input yang diperoleh dari string kueri, dan juga respons dari server. Jika berhasil, metode ini akan mengembalikan instance kelas Bor:

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




Kami mendapat deskripsi yang bagus. Menyombongkan diri dengan menebak-nebak objek dengan bagaimana serial, dengan satu pengecualian: bidang awal dan akhir dalam kelas Drill kita adalah objek Instan, dan serial dalam Long. Saya ingin mengganti 0 dengan nilai yang lebih cocok. Kita dapat melakukan ini dengan menerapkan penjelasan @ApiModel, @ApiModelProperty ke kelas kami:

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


Sekarang kami memiliki deskripsi model yang benar-benar benar:




Contoh 2


Untuk menjelaskan metode Posting, di mana parameter input dilewatkan di badan permintaan, anotasi @ApiImplicitParams digunakan:

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

Contoh 3


Sejauh ini, semuanya sederhana. Ini adalah contoh yang lebih kompleks. Misalkan ada kelas umum tergantung pada parameter tipe:

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

Kesombongan belum mengerti obat generik, setidaknya. Kami tidak dapat menunjukkan dalam anotasi:

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


Satu-satunya cara dalam situasi ini adalah dengan subkelas tipe generik untuk masing-masing tipe yang kita butuhkan. Sebagai contoh, kita dapat mensubklasifikasikan DrillSessionedResponse.
Satu-satunya masalah adalah, kita tidak bisa mewarisi dari kelas kasus. Untungnya, dalam proyek saya, tidak ada yang mencegah saya mengubah kelas kasus menjadi kelas. Kemudian:

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)

Sekarang saya dapat menentukan kelas ini dalam anotasi:

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

Contoh 4


Sekarang contoh yang lebih kompleks terkait dengan ADT - tipe data aljabar.

Swagger menyediakan mekanisme untuk bekerja dengan ADT:

Abstrak @ApiModel memiliki 2 opsi untuk tujuan ini:

1. subType - enumerasi subclass

2. DISCRIMINATOR - bidang di mana sub-kelas berbeda satu sama lain.

Dalam kasus saya, uPickle, memproduksi JSON dari kelas kasus, menambahkan bidang $ type itu sendiri, dan case - membuat serialisasi objek menjadi string. Jadi pendekatan dengan bidang diskriminator tidak dapat diterima.

Saya menggunakan pendekatan yang berbeda. Katakanlah ada

sealed trait Permission

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


di mana Obj adalah ADT lain yang terdiri dari objek kasus:

//  permission.drill
case object DrillObj extends Obj

// permission.team
case object TeamObj extends Obj


Agar Swagger dapat memahami model ini, alih-alih kelas nyata (atau sifat), ia perlu menyediakan kelas yang dibuat khusus untuk tujuan ini dengan bidang-bidang yang diperlukan:

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

Sekarang kita harus menentukan FakePermission dalam anotasi alih-alih Izin

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

Koleksi


Hal terakhir yang saya ingin menarik perhatian pembaca. Seperti yang saya katakan, kesombongan tidak mengerti tipe generik. Namun, ia tahu cara bekerja dengan koleksi.

Jadi, anotasi @ApiOperation memiliki parameter responseContainer yang Anda dapat memberikan nilai "Daftar".

Mengenai parameter input, sebuah indikasi

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

dalam anotasi yang mendukung atribut ini, menghasilkan hasil yang diinginkan. Meskipun, jika Anda menentukan scala.collection.List - tidak berfungsi.

Kesimpulan


Dalam proyek saya, menggunakan penjelasan Swagger-Core, saya dapat sepenuhnya menggambarkan Rest-API dan semua model data, termasuk tipe generik dan tipe data aljabar. Menurut pendapat saya, menggunakan modul Swagger-Play optimal untuk menghasilkan deskripsi API secara otomatis.

All Articles