إضافة Swagger إلى واجهتك الخاصة بـ Go

توليد وثائق OpenAPI تلقائيًا من التعليقات في الكود

Page content

توثيق API ضروري لأي تطبيق حديث، وعندما يتعلق الأمر بـ Go APIs Swagger (OpenAPI) فقد أصبح المعيار الصناعي.

للمطورين في Go، يوفر swaggo حلًا أنيقًا لإنشاء وثائق API شاملة مباشرة من التعليقات في الكود.

swagger api specs on agile board هذا الصورة الجميلة تُنتجها AI model Flux 1 dev.

لماذا يهم Swagger لـ Go APIs

عند بناء واجهات برمجة التطبيقات REST، تصبح الوثائق غالبًا قديمة مع تطور الكود. يحل Swagger هذا المشكل من خلال إنشاء وثائق من مصدر الكود الخاص بك، مما يضمن أنها تبقى متزامنة مع تنفيذك. واجهة Swagger التفاعلية تسمح للمطورين بإجراء اختبارات على نقاط النهاية مباشرة من المتصفح، مما يحسن بشكل كبير تجربة المطور.

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

  • إنشاء مكتبات العملاء: إنشاء مكتبات العملاء تلقائيًا بلغات متعددة
  • اختبار العقد: التحقق من طلبات وردود الفعل ضد المخططات المحددة
  • التعاون بين الفرق: توفير مصدر واحد للحقيقة لعقود API
  • التوظيف للمطورين الجدد: يمكن للموظفين الجدد استكشاف واجهات API تفاعليًا

البدء مع swaggo

مكتبة swaggo هي الأداة الأكثر شيوعًا لإضافة دعم Swagger لتطبيقات Go. تعمل من خلال تحليل التعليقات الخاصة في الكود الخاص بك وتحقيق ملفات تحديد OpenAPI 3.0.

التثبيت

أولًا، تثبيت أداة سواغ CLI:

go install github.com/swaggo/swag/cmd/swag@latest

ثم أضف حزمة الوسيط Swagger المناسبة لمنصةك. لـ Gin:

go get -u github.com/swaggo/gin-swagger
go get -u github.com/swaggo/files

لـ Echo:

go get -u github.com/swaggo/echo-swagger

لـ Fiber:

go get -u github.com/gofiber/swagger

التكوين الأساسي

ابدأ بإضافة معلومات API العامة في ملف main.go. بطريقة مشابهة لكيفية ترتيبك لـ REST API في Go, يجب أن تكون التعليقات واضحة ووصفية:

// @title           Product API
// @version         1.0
// @description     API لإدارة المنتجات مع وثائق Swagger
// @termsOfService  http://swagger.io/terms/

// @contact.name   دعم API
// @contact.url    http://www.swagger.io/support
// @contact.email  support@swagger.io

// @license.name  Apache 2.0
// @license.url   http://www.apache.org/licenses/LICENSE-2.0.html

// @host      localhost:8080
// @BasePath  /api/v1

// @securityDefinitions.apikey Bearer
// @in header
// @name Authorization
// @description أدخل "Bearer" متبوعًا بمسافة ورمز JWT.

func main() {
    // كود تطبيقك هنا
}

تنفيذ مع إطارة Gin

لنقم بتنفيذ مثال كامل باستخدام Gin. أولاً، قم بتعريف نماذج البيانات مع علامات المخطط:

type Product struct {
    ID          int     `json:"id" example:"1"`
    Name        string  `json:"name" example:"Laptop" binding:"required"`
    Description string  `json:"description" example:"High-performance laptop"`
    Price       float64 `json:"price" example:"999.99" binding:"required,gt=0"`
    Stock       int     `json:"stock" example:"50"`
}

type ErrorResponse struct {
    Error   string `json:"error" example:"Invalid input"`
    Message string `json:"message" example:"Product name is required"`
}

الآن أضف تعليقات وظائف المُعالجة. عند العمل مع عمليات قاعدة البيانات, هذه التعليقات تساعد في وثيق تدفق البيانات:

// GetProduct godoc
// @Summary      الحصول على منتج حسب ID
// @Description  استرداد منتج واحد حسب معرفه الفريد
// @Tags         المنتجات
// @Accept       json
// @Produce      json
// @Param        id   path      int  true  "معرف المنتج"
// @Success      200  {object}  Product
// @Failure      400  {object}  ErrorResponse
// @Failure      404  {object}  ErrorResponse
// @Router       /products/{id} [get]
func GetProduct(c *gin.Context) {
    id := c.Param("id")
    // تنفيذ هنا
    c.JSON(200, Product{ID: 1, Name: "Laptop", Price: 999.99})
}

// CreateProduct godoc
// @Summary      إنشاء منتج جديد
// @Description  إضافة منتج جديد إلى الكتالوج
// @Tags         المنتجات
// @Accept       json
// @Produce      json
// @Param        product  body      Product  true  "كائن المنتج"
// @Success      201      {object}  Product
// @Failure      400      {object}  ErrorResponse
// @Security     Bearer
// @Router       /products [post]
func CreateProduct(c *gin.Context) {
    var product Product
    if err := c.ShouldBindJSON(&product); err != nil {
        c.JSON(400, ErrorResponse{Error: "طلب خاطئ", Message: err.Error()})
        return
    }
    // حفظ في قاعدة البيانات
    c.JSON(201, product)
}

إنشاء الوثائق

بعد إضافة التعليقات إلى الكود، أنشئ وثائق Swagger:

swag init

هذا ينشئ مجلدًا يُسمى docs مع ملفات swagger.json و swagger.yaml وملفات Go. استورد وسجّل نقطة النهاية الخاصة بـ Swagger:

package main

import (
    "github.com/gin-gonic/gin"
    swaggerFiles "github.com/swaggo/files"
    ginSwagger "github.com/swaggo/gin-swagger"
    
    _ "yourproject/docs" // استيراد الوثائق المُولدة
)

func main() {
    r := gin.Default()
    
    // مسارات API
    v1 := r.Group("/api/v1")
    {
        v1.GET("/products/:id", GetProduct)
        v1.POST("/products", CreateProduct)
    }
    
    // نقطة النهاية الخاصة بـ Swagger
    r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
    
    r.Run(":8080")
}

الآن قم بزيارة وثائق API التفاعلية الخاصة بك على http://localhost:8080/swagger/index.html.

تنفيذ مع إطارة Echo

يستخدم مستخدمي Echo نمطًا مشابهًا لكنه يعتمد على الوسيط الخاص بـ Echo:

package main

import (
    "github.com/labstack/echo/v4"
    echoSwagger "github.com/swaggo/echo-swagger"
    
    _ "yourproject/docs"
)

func main() {
    e := echo.New()
    
    // مسارات API
    api := e.Group("/api/v1")
    api.GET("/products/:id", getProduct)
    api.POST("/products", createProduct)
    
    // نقطة النهاية الخاصة بـ Swagger
    e.GET("/swagger/*", echoSwagger.WrapHandler)
    
    e.Start(":8080")
}

تنفيذ مع إطارة Fiber

تنفيذ Fiber بسيط بنفس القدر:

package main

import (
    "github.com/gofiber/fiber/v2"
    "github.com/gofiber/swagger"
    
    _ "yourproject/docs"
)

func main() {
    app := fiber.New()
    
    // مسارات API
    api := app.Group("/api/v1")
    api.Get("/products/:id", getProduct)
    api.Post("/products", createProduct)
    
    // نقطة النهاية الخاصة بـ Swagger
    app.Get("/swagger/*", swagger.HandlerDefault)
    
    app.Listen(":8080")
}

التعليقات المُتقدمة لـ Swagger

وثيق جسم الطلب المعقد

للمستрукات المُستضمنة أو المصفوفات:

type CreateOrderRequest struct {
    CustomerID int           `json:"customer_id" example:"123" binding:"required"`
    Items      []OrderItem   `json:"items" binding:"required,min=1"`
    ShippingAddress Address  `json:"shipping_address" binding:"required"`
}

type OrderItem struct {
    ProductID int `json:"product_id" example:"1" binding:"required"`
    Quantity  int `json:"quantity" example:"2" binding:"required,min=1"`
}

type Address struct {
    Street  string `json:"street" example:"123 Main St" binding:"required"`
    City    string `json:"city" example:"New York" binding:"required"`
    ZipCode string `json:"zip_code" example:"10001" binding:"required"`
}

// CreateOrder godoc
// @Summary      إنشاء طلب جديد
// @Description  إنشاء طلب مع عدة عناصر ومعلومات الشحن
// @Tags         الطلبات
// @Accept       json
// @Produce      json
// @Param        order  body      CreateOrderRequest  true  "تفاصيل الطلب"
// @Success      201    {object}  Order
// @Failure      400    {object}  ErrorResponse
// @Failure      422    {object}  ErrorResponse
// @Security     Bearer
// @Router       /orders [post]
func CreateOrder(c *gin.Context) {
    // تنفيذ هنا
}

وثيق تحميل الملفات

// UploadImage godoc
// @Summary      تحميل صورة منتج
// @Description  تحميل ملف صورة لمنتج
// @Tags         المنتجات
// @Accept       multipart/form-data
// @Produce      json
// @Param        id    path      int   true  "معرف المنتج"
// @Param        file  formData  file  true  "ملف الصورة"
// @Success      200   {object}  map[string]string
// @Failure      400   {object}  ErrorResponse
// @Security     Bearer
// @Router       /products/{id}/image [post]
func UploadImage(c *gin.Context) {
    file, _ := c.FormFile("file")
    // معالجة التحميل
}

المعلمات الاستعلامية وتصفح البيانات

// ListProducts godoc
// @Summary      قائمة المنتجات مع التصفح
// @Description  الحصول على قائمة من المنتجات مع تصفح اختياري
// @Tags         المنتجات
// @Accept       json
// @Produce      json
// @Param        page      query     int     false  "رقم الصفحة" default(1)
// @Param        page_size query     int     false  "عدد العناصر لكل صفحة" default(10)
// @Param        category  query     string  false  "فلترة حسب الفئة"
// @Param        min_price query     number  false  "السعر الأدنى"
// @Param        max_price query     number  false  "السعر الأقصى"
// @Success      200       {array}   Product
// @Failure      400       {object}  ErrorResponse
// @Router       /products [get]
func ListProducts(c *gin.Context) {
    // تنفيذ مع التصفح
}

المصادقة والأمان

وثق طرق المصادقة المختلفة في API الخاص بك. لـ تطبيقات متعددة المستأجرين, وثيقة المصادقة مناسبة للغاية:

مصادقة Bearer Token

// @securityDefinitions.apikey Bearer
// @in header
// @name Authorization
// @description أدخل "Bearer" متبوعًا بمسافة ورمز JWT.

مصادقة مفتاح API

// @securityDefinitions.apikey ApiKeyAuth
// @in header
// @name X-API-Key
// @description مفتاح API للمصادقة

مصادقة OAuth2

// @securitydefinitions.oauth2.application OAuth2Application
// @tokenUrl https://example.com/oauth/token
// @scope.write يمنحك الوصول للكتابة
// @scope.admin يمنحك الوصول للقراءة والكتابة إلى المعلومات الإدارية

مصادقة Basic

// @securityDefinitions.basic BasicAuth

تطبيق المصادقة على نقاط النهاية المحددة:

// @Security Bearer
// @Security ApiKeyAuth

تخصيص واجهة Swagger UI

يمكنك تخصيص مظهر وسلوك واجهة Swagger UI:

// التكوين المخصص
url := ginSwagger.URL("http://localhost:8080/swagger/doc.json")
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, url))

// مع عنوان مخصص
r.GET("/swagger/*any", ginSwagger.WrapHandler(
    swaggerFiles.Handler,
    ginSwagger.URL("http://localhost:8080/swagger/doc.json"),
    ginSwagger.DefaultModelsExpandDepth(-1),
))

لتعطيل Swagger في الإنتاج:

if os.Getenv("ENV") != "production" {
    r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
}

دمج مع CI/CD

تلقين إنشاء وثائق Swagger في أنبوب CI/CD الخاص بك:

# مثال GitHub Actions
name: Generate Swagger Docs
on: [push]

jobs:
  swagger:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Set up Go
        uses: actions/setup-go@v4
        with:
          go-version: '1.21'
      
      - name: Install swag
        run: go install github.com/swaggo/swag/cmd/swag@latest
      
      - name: Generate Swagger docs
        run: swag init
      
      - name: Commit docs
        run: |
          git config user.name github-actions
          git config user.email github-actions@github.com
          git add docs/
          git commit -m "Update Swagger documentation" || exit 0
          git push          

أفضل الممارسات

1. نمط تعليقات متسق

حافظ على التنسيق المتسق عبر جميع نقاط النهاية:

// HandlerName godoc
// @Summary      وصف مختصر (أقل من 50 حرف)
// @Description  وصف تفصيلي لما تفعله نقطة النهاية
// @Tags         اسم المورد
// @Accept       json
// @Produce      json
// @Param        name  location  type  required  "وصف"
// @Success      200   {object}  ResponseType
// @Failure      400   {object}  ErrorResponse
// @Router       /path [method]

2. استخدام أمثلة وصفية

أضف أمثلة واقعية لمساعد API المستهلكين:

type User struct {
    ID        int       `json:"id" example:"1"`
    Email     string    `json:"email" example:"user@example.com"`
    CreatedAt time.Time `json:"created_at" example:"2025-01-15T10:30:00Z"`
}

3. وثيق جميع رموز الاستجابة

شامل جميع رموز HTTP الممكنة:

// @Success      200  {object}  Product
// @Success      201  {object}  Product
// @Failure      400  {object}  ErrorResponse "طلب خاطئ"
// @Failure      401  {object}  ErrorResponse "غير مُخوّل"
// @Failure      403  {object}  ErrorResponse "محظور"
// @Failure      404  {object}  ErrorResponse "غير موجود"
// @Failure      422  {object}  ErrorResponse "خطأ في التحقق"
// @Failure      500  {object}  ErrorResponse "خطأ داخلي في الخادم"

4. إصدار API

استخدم الإصدار المناسب في مسار القاعدة:

// @BasePath  /api/v1

وتنظيم الكود وفقًا لذلك:

v1 := r.Group("/api/v1")
v2 := r.Group("/api/v2")

5. تجميع نقاط النهاية المرتبطة

استخدم الوسوم لتجميع نقاط النهاية منطقيًا:

// @Tags products
// @Tags orders
// @Tags users

6. الحفاظ على تحديث الوثائق

تشغيل swag init قبل كل التزام أو دمجها في عملية البناء الخاصة بك:

#!/bin/bash
# hook التزام
swag init
git add docs/

اختبار وثائق Swagger

عند العمل مع هياكل خادم غير مركزي مثل AWS Lambda, يصبح اختبار وثائق API أكثر أهمية:

func TestSwaggerGeneration(t *testing.T) {
    // تحقق من وجود swagger.json
    _, err := os.Stat("./docs/swagger.json")
    if err != nil {
        t.Fatal("swagger.json غير موجود، قم بتشغيل 'swag init'")
    }
    
    // تحقق من نقطة النهاية الخاصة بـ swagger
    r := setupRouter()
    w := httptest.NewRecorder()
    req, _ := http.NewRequest("GET", "/swagger/index.html", nil)
    r.ServeHTTP(w, req)
    
    assert.Equal(t, 200, w.Code)
}

المشاكل الشائعة والحلول

عدم تحديث الوثائق

إذا لم تظهر التغييرات، تأكد من إعادة إنشاء الوثائق:

swag init --parseDependency --parseInternal

العلم --parseDependency يحلل الاعتماديات الخارجية، والعلم --parseInternal يحلل الحزم الداخلية.

أنواع مخصصة غير مُعرفة

للمواد من الحزم الخارجية، استخدم علامة swaggertype:

type CustomTime struct {
    time.Time
}

func (CustomTime) SwaggerDoc() map[string]string {
    return map[string]string{
        "time": "مقطع زمني RFC3339",
    }
}

أو استخدم علامة swaggertype:

type Product struct {
    ID        int        `json:"id"`
    UpdatedAt CustomTime `json:"updated_at" swaggertype:"string" format:"date-time"`
}

المصفوفات والقيم المحددة

وثق أنواع المصفوفات والقيم المحددة:

type Filter struct {
    Status []string `json:"status" enums:"active,inactive,pending"`
    Tags   []string `json:"tags"`
}

// @Param  status  query  string  false  "فلترة الحالة" Enums(active, inactive, pending)

طرق بديلة

بينما swaggo هو الخيار الأكثر شيوعًا، توجد طرق أخرى:

go-swagger

خيار أكثر تقدمًا ولكن معقد:

brew install go-swagger
swagger generate spec -o ./swagger.json

ملفات OpenAPI يدوية

للحصول على التحكم الكامل، اكتب تحديدات OpenAPI يدويًا في YAML:

openapi: 3.0.0
info:
  title: Product API
  version: 1.0.0
paths:
  /products:
    get:
      summary: قائمة المنتجات
      responses:
        '200':
          description: نجاح

ثم قم بتقديمه مع:

r.StaticFile("/openapi.yaml", "./openapi.yaml")

دمج مع الذكاء الاصطناعي والLLMs

عند بناء واجهات برمجة التطبيقات التي تتكامل مع خدمات الذكاء الاصطناعي، تصبح الوثائق المناسبة ضرورية. على سبيل المثال، عند العمل مع مخرجات LLM المُهيكلة, تساعد وثائق Swagger في وثيق أنواع طلبات وردود المخططات المعقدة:

type LLMRequest struct {
    Prompt      string            `json:"prompt" example:"قم بتلخيص هذا النص"`
    Model       string            `json:"model" example:"qwen2.5:latest"`
    Temperature float64           `json:"temperature" example:"0.7" minimum:"0" maximum:"2"`
    MaxTokens   int               `json:"max_tokens" example:"1000" minimum:"1"`
    Schema      map[string]interface{} `json:"schema,omitempty"`
}

// GenerateStructured godoc
// @Summary      إنشاء مخرجات LLM مُهيكلة
// @Description  إنشاء نص مع مخطط مخرجات مُقيد
// @Tags         llm
// @Accept       json
// @Produce      json
// @Param        request  body      LLMRequest  true  "parameters LLM"
// @Success      200      {object}  map[string]interface{}
// @Failure      400      {object}  ErrorResponse
// @Router       /llm/generate [post]
func GenerateStructured(c *gin.Context) {
    // تنفيذ هنا
}

اعتبارات الأداء

لدي وثائق Swagger تأثيرًا بسيطًا على الأداء:

  • وقت البناء: swag init يستغرق 1-3 ثوانٍ لمعظم المشاريع
  • الوقت أثناء التشغيل: تُحمل الوثائق مرة واحدة عند بدء التشغيل
  • الذاكرة: تضيف عادةً 1-2 ميجابايت إلى حجم الملف
  • وقت الاستجابة: لا يؤثر على نقاط النهاية الخاصة بالواجهات برمجة التطبيقات نفسها

للتطبيقات الكبيرة جدًا (100+ نقطة نهائية)، فكّر في:

  • تقسيمها إلى ملفات Swagger متعددة
  • تحميل Swagger UI الأصول بشكل متأخر
  • تقديم الوثائق من خدمة منفصلة

اعتبارات الأمان

عند كشف وثائق Swagger:

  1. تعطيلها في الإنتاج (إذا كانت الواجهة برمجة التطبيقات داخليًا):
if os.Getenv("ENV") == "production" {
    // لا تُسجّل نقطة النهاية الخاصة بـ Swagger
    return
}
  1. إضافة المصادقة:
authorized := r.Group("/swagger")
authorized.Use(AuthMiddleware())
authorized.GET("/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
  1. تقييد الوصول إلى نقطة النهاية:
r.GET("/swagger/*any", RateLimitMiddleware(), ginSwagger.WrapHandler(swaggerFiles.Handler))
  1. لا تكشف تفاصيل داخلية أبدًا:
  • لا توثق نقاط النهاية الداخلية
  • تجنب كشف مخططات قواعد البيانات مباشرة
  • تطهير رسائل الخطأ في الوثائق

الخاتمة

إضافة وثائق Swagger إلى واجهة برمجة تطبيقات Go تُحوّل تجربة المطور من التخمين إلى الاستكشاف المُوجه. تجعل مكتبة swaggo من هذا العملية بسيطة من خلال إنشاء وثائق OpenAPI الشاملة من تعليقات الكود الخاصة بك.

الملاحظات الرئيسية:

  • ابدأ بتعليقات أساسية وتوسع تدريجيًا
  • الحفاظ على تزامن الوثائق مع الكود عبر CI/CD
  • استخدام واجهة Swagger التفاعلية أثناء التطوير
  • وثيق المصادقة، الأخطاء، والحالات الحدودية بشكل شامل
  • تفكير في اعتبارات الأمان عند كشف الوثائق

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

للمطورين في Go، الجمع بين التипات القوية، إنشاء الكود، ونظام swaggo التعليقات يخلق تدفقًا قويًا يجعل وثائق API جزءًا طبيعيًا من عملية التطوير بدلًا من أن تكون بعدًا.

الروابط المفيدة

الموارد الخارجية