افتح لعبة API: Swagger Play


في هذه المقالة ، أريد أن أخبرك عن كيفية استخدام وحدة Swagger لإطار عمل Play ، مع أمثلة واقعية. انا ساخبر:

  1. كيفية ربط أحدث إصدار من Swagger-Play (وحدة تشغيل تسمح لك باستخدام التعليقات التوضيحية لـ swagger-api وإنشاء الوثائق بناءً على مواصفات OpenAPI) وكيفية تكوين swagger-ui (مكتبة جافا سكريبت المستخدمة لتصور الوثائق التي تم إنشاؤها)
  2. سأصف التعليقات التوضيحية الرئيسية لـ Swagger-Core وأتحدث عن ميزات استخدامها لـ Scala
  3. سأخبرك بكيفية العمل مع فئات نماذج البيانات بشكل صحيح
  4. كيفية التغلب على مشكلة النوع العام في Swagger ، والتي لا تعرف كيفية العمل مع الأدوية الجنيسة
  5. كيفية تعليم Swagger لفهم ADT (أنواع البيانات الجبرية)
  6. كيفية وصف المجموعات

ستكون المقالة مثيرة للاهتمام لكل من يستخدم Play Framework على Scala وسيعمل على أتمتة توثيق واجهة برمجة التطبيقات.

أضف التبعية


بعد دراسة العديد من المصادر على الإنترنت ، أخلص إلى أنه من أجل تكوين صداقات Swagger و Play Framework ، تحتاج إلى تثبيت وحدة Swagger Play2.

عنوان المكتبة على github:

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

أضف التبعية:

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

وهنا تبرز المشكلة:

في وقت كتابة هذا التقرير ، لم يتم سحب التبعية من مستودعات Maven المركزية أو مستودعات Sonatype.

في Maven-central ، انتهت جميع البنى الموجودة في Scala 2.12. بشكل عام ، لم يكن هناك نسخة مجمعة واحدة ل Scala 2.13.

آمل حقًا أن تظهر في المستقبل.

أثناء تسلقي مستودع إصدارات Sonatype ، وجدت الشوكة الحالية لهذه المكتبة. العنوان على github:

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

لذا ، نقوم بإدراج التبعية:

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

إضافة مستودع Sonatype:

resolvers += Resolver.sonatypeRepo("releases")

(ليس ضروريًا ، نظرًا لأن هذا التجميع هو Maven-central)

يبقى الآن تنشيط الوحدة النمطية في ملف التكوين application.conf

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

بالإضافة إلى إضافة مسار إلى المسارات:

GET     /swagger.json           controllers.ApiHelpController.getResources

والوحدة جاهزة للانطلاق.

الآن ستقوم وحدة Swagger Play بإنشاء ملف json يمكن عرضه في المستعرض.

للاستمتاع الكامل بميزات Swagger ، تحتاج أيضًا إلى تنزيل مكتبة التصور: swagger-ui. يوفر واجهة رسومية ملائمة لقراءة ملف swagger.json ، بالإضافة إلى القدرة على إرسال طلبات الراحة إلى الخادم ، مما يوفر بديلاً ممتازًا لـ Postman و Rest-client وأدوات أخرى مماثلة.

لذا ، أضف حسب:

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

في وحدة التحكم ، نقوم بإنشاء طريقة تعيد توجيه المكالمات إلى ملف index.html للمكتبة الثابتة:

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

حسنًا ، نحن نصف الطريق في ملف المسارات:

GET   /docs                   controllers.HomeController.redirectDocs()

بالطبع ، تحتاج إلى ربط مكتبة لعب webjars. إضافة حسب:

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

وأضف المسار إلى ملف المسارات:

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

شريطة أن يكون تطبيقنا قيد التشغيل ، نكتب في المتصفح

http: // localhost: 9000 / docs

، وإذا تم عمل كل شيء بشكل صحيح ، فسننتقل إلى صفحة التبجح في تطبيقنا:



لا تحتوي الصفحة حتى الآن على بيانات حول rest-api. لتغيير هذا ، تحتاج إلى استخدام التعليقات التوضيحية ، والتي سيتم فحصها بواسطة وحدة Swagger-Play.

شروح


يمكن العثور على وصف مفصل لجميع التعليقات التوضيحية الخاصة بـ swagger-api-core على:

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

في مشروعي استخدمت التعليقات التوضيحية التالية:

@Api - تلاحظ فئة وحدة التحكم كمورد التبختر (للمسح الضوئي)

@ApiImplicitParam - يصف المعلمة "الضمنية" (على سبيل المثال، المحددة في نص الطلب)

@ApiImplicitParams - بمثابة وعاء لعدة @ApiImplicitParam الشروح

@ApiModel - يسمح لك لوصف

@ApiModelProperty بيانات نموذج - يصف ويفسر

@ApiOperation حقل فئة نموذج البيانات - يصف طريقة تحكم (ربما التعليق التوضيحي الرئيسي في هذه القائمة)

@ApiParam- يصف معلمة الطلب المحددة صراحة (في سلسلة الاستعلام ، على سبيل المثال)

@ApiResponse - يصف استجابة الخادم لطلب

@ApiResponses - يعمل كحاوية للعديد من التعليقات التوضيحية لـ @ApiResponse. عادة ما تتضمن إجابات إضافية (على سبيل المثال ، عندما تحدث رموز الخطأ). عادة ما يتم وصف الاستجابة الناجحة في التعليق التوضيحي لـ @ApiOperation.

لذا ، لكي تتمكن Swagger من فحص فئة وحدة التحكم ، تحتاج إلى إضافة التعليق التوضيحي لـ @Api

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

هذا يكفي لـ Swagger للعثور على المسارات المتعلقة بأساليب التحكم في ملف التوجيه ومحاولة وصفها.



ولكن من الواضح أن تحديد فئة وحدة تحكم Swagger ليس كافيًا. Swagger ينتظرنا مع التعليقات التوضيحية الأخرى.

لماذا لا يستطيع Swagger القيام بذلك تلقائيًا؟ لأنه ليس لديه فكرة عن كيفية تسلسل دروسنا. في هذا المشروع أستخدم uPickle ، شخص ما يستخدم Circe ، شخص ما Play-JSON. لذلك ، يجب عليك توفير روابط للفصول المستلمة والمصدرة.

نظرًا لأن المكتبة المستخدمة مكتوبة بلغة جافا ، فهناك العديد من الفروق الدقيقة في مشروع سكالا.

أول شيء يجب عليك التعامل معه هو بناء الجملة: التعليقات التوضيحية المتداخلة لا تعمل.

على سبيل المثال ، كود Java:

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


في Scala ستبدو كما يلي:

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


مثال 1


لذا ، دعنا نصف طريقة تحكم تبحث عن كيان في قاعدة البيانات:

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


باستخدام التعليقات التوضيحية ، يمكننا تحديد وصف للأسلوب ، ومعلمة إدخال تم الحصول عليها من سلسلة الاستعلام ، وأيضًا ردود من الخادم. في حالة نجاحها ، ستعرض الطريقة مثيل لفئة الحفر:

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




لدينا وصف جيد خمنت Swagger تقريبًا ما تم إجراء تسلسل للكائن به ، باستثناء واحد: حقلي البداية والنهاية في فئة Drill الخاص بنا هما كائنات فورية ، ومسلسلة في Long. أود استبدال 0 بقيم أكثر ملاءمة. يمكننا القيام بذلك من خلال تطبيق التعليقات التوضيحيةApiModel وApiModelProperty على فصلنا:

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


الآن لدينا وصف صحيح تمامًا للنموذج:




مثال 2


لوصف طريقة Post ، حيث يتم تمرير معلمة الإدخال في نص الطلب ، يتم @استخدام التعليق التوضيحي 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 =>

مثال 3


حتى الآن ، كان كل شيء بسيطًا. هنا مثال أكثر تعقيدًا. افترض أن هناك فئة معممة اعتمادًا على معلمة النوع:

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

Swagger لا يفهم الأدوية الجنيسة بعد ، على الأقل. لا يمكننا الإشارة في التعليق التوضيحي:

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


الطريقة الوحيدة في هذه الحالة هي تصنيف فئة فرعية لكل نوع من الأنواع التي نحتاجها. على سبيل المثال ، يمكننا فئة فرعية DrillSessionedResponse.
المشكلة الوحيدة هي أننا لا نستطيع أن نرث من فئة الحالة. لحسن الحظ ، في مشروعي ، لا شيء يمنعني من تغيير فئة الحالة إلى فئة. ثم:

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)

يمكنني الآن تحديد هذا الفصل الدراسي في التعليق التوضيحي:

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

مثال 4


الآن مثال أكثر تعقيدًا يتعلق بـ ADT - أنواع البيانات الجبرية.

يوفر التبختر آلية للعمل مع ADT:

ملخص @ApiModel ديه 2 خيارات لهذا الغرض:

1. فرعية - تعداد الفئات الفرعية

2. الممي - حقل الذي فرعية تختلف عن بعضها البعض.

في حالتي ، يضيف uPickle ، الذي ينتج JSON من فئات الحالة ، حقل $ type نفسه ، ويؤدي تسلسل الحالة إلى كائنات إلى سلاسل. لذلك لم يكن النهج مع مجال التمييز مقبولاً.

لقد استخدمت نهج مختلف. دعنا نقول أن هناك

sealed trait Permission

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


حيث Obj هو ADT آخر يتكون من كائنات الحالة:

//  permission.drill
case object DrillObj extends Obj

// permission.team
case object TeamObj extends Obj


لكي يفهم Swagger هذا النموذج ، بدلاً من فئة حقيقية (أو سمة) ، يحتاج إلى توفير فئة تم إنشاؤها خصيصًا لهذا الغرض مع الحقول الضرورية:

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

الآن يجب علينا تحديد FakePermission في التعليق التوضيحي بدلاً من الإذن

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

مجموعات


آخر شيء أردت أن ألفت انتباه القراء. كما قلت ، لا يفهم Swagger الأنواع العامة. ومع ذلك ، فهو يعرف كيفية العمل مع المجموعات.

لذلك ، @يحتوي التعليق التوضيحي ApiOperation على معلمة responseContainer التي يمكنك تمرير القيمة "List" إليها.

فيما يتعلق بمعلمات الإدخال ، إشارة

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

ضمن التعليقات التوضيحية التي تدعم هذه السمة ، تنتج النتائج المرجوة. على الرغم من ذلك ، إذا قمت بتحديد scala.collection.List - لا يعمل.

استنتاج


في مشروعي ، باستخدام شروح Swagger-Core ، تمكنت من وصف Rest-API وجميع نماذج البيانات بشكل كامل ، بما في ذلك الأنواع العامة وأنواع البيانات الجبرية. في رأيي ، فإن استخدام وحدة Swagger-Play هو الأمثل لإنشاء أوصاف API تلقائيًا.

All Articles