Как интегрировать Swagger UI в Go Backend — Gin Edition


Введение

В отличие от FastAPI, Gin не имеет встроенной интеграции OpenAPI. В FastAPI при добавлении маршрута уже создается документация. В Gin это не так. Но недавно я интегрировал Swagger UI в один из своих бэкендов Go и хотел задокументировать этот процесс.

Необходимые условия

  • Уже существующий сервер Gin

Если вы начинаете с нуля, вы можете рассмотреть статью Building a Book Store API in Golang With Gin. Я буду использовать тот же книжный магазин и расширять его для интеграции Swagger UI.

Как Swagger работает с Gin

То, как Swagger работает с другими бэкендами Go, не отличается, у всех один и тот же механизм. Но как это работает на самом деле и что нам нужно делать на самом деле?

Swagger использует систему комментариев Go, которая, как мы знаем, очень хорошо интегрирована с документацией. Мы пишем комментарии заранее определенным способом, подробности которого мы увидим далее в посте. Но в основном все делится на 2 части. Сам сервер и маршруты.

Swagger имеет бинарник CLI, который при запуске преобразует эти документы комментариев в документацию, совместимую с OpenAPI. Результирующий файл также включает спецификации сервера OpenAPI в формате JSON и YAML.

Наконец, мы направляем сгенерированный контент через обработчик в нашем бэкенде.

Как мы это делаем? Давайте посмотрим.

Настройка Swagger CLI

Главный репозиторий, который вы должны держать под подушкой, — это https://github.com/swaggo/swag. Как в плане CLI, так и в плане документации.

Мы установим CLI-приложение под названием swag. Вот как:

go get -u github.com/swaggo/swag/cmd/swag

# 1.16 or newer
go install github.com/swaggo/swag/cmd/swag@latest
Войти в полноэкранный режим Выйти из полноэкранного режима

Первая команда загрузит зависимости для интеграции в серверное приложение.

Вторая команда установит CLI.

Теперь вы должны быть в состоянии запустить команду swag:

$ swag -h
NAME:
   swag - Automatically generate RESTful API documentation with Swagger 2.0 for Go.

USAGE:
   swag [global options] command [command options] [arguments...]

VERSION:
   v1.8.1

COMMANDS:
   init, i  Create docs.go
   fmt, f   format swag comments
   help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --help, -h     show help (default: false)
   --version, -v  print the version (default: false)

Войти в полноэкранный режим Выйти из полноэкранного режима

Документирование сервера Gin

Если вы посмотрите на документацию Swagger UI для любого сайта с поддержкой OpenAPI, вы увидите нечто подобное:

Вы можете быть знакомы с нижней половиной пользовательского интерфейса. Именно для этого и предназначена документация Swagger, т. е. для документирования маршрутов.

Но прежде чем мы разберемся с нижней 50%-ной частью, я хочу, чтобы вы обратили внимание на верхнюю 50%-ную часть на рисунке выше. Прямо от заголовка Swagger Petstore до ссылки Find out more about Swagger anchor.

Эта часть показывает метаданные о самом сервере API. В этом разделе мы собираемся построить эту часть. Вы можете найти код для начала здесь. Мы продолжим работу над этим кодом.

Прежде всего, нам нужно получить пакеты, с которыми мы должны работать:

go get -u github.com/swaggo/files
go get -u github.com/swaggo/gin-swagger
Вход в полноэкранный режим Выйти из полноэкранного режима

Теперь, после того как мы это сделали, давайте посмотрим на наш файл main.go. На данный момент это main.go:

package main

import "github.com/santosh/gingo/routes"

func main() {
    router := routes.SetupRouter()

    router.Run(":8080")
}
Вход в полноэкранный режим Выход из полноэкранного режима

Добавление маршрута для Swagger Docs

Мы добавляем новый маршрутизатор в функции main:

router.GET("/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
Вход в полноэкранный режим Выход из полноэкранного режима

Измененный файл будет выглядеть следующим образом:

-import "github.com/santosh/gingo/routes"
+import (
+       "github.com/santosh/gingo/routes"
+       swaggerFiles "github.com/swaggo/files"
+       ginSwagger "github.com/swaggo/gin-swagger"
+)

 func main() {
        router := routes.SetupRouter()

+       router.GET("/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
+
        router.Run(":8080")
 }
Вход в полноэкранный режим Выход из полноэкранного режима

swaggerFiles.Handler здесь находится обработчик, в который встроены все активы. Такие активы, как файлы HTML, CSS и javascript, отображаются в документации.

ginSwagger.WrapHandler — это обертка вокруг http.Handler, но для Gin.

Этого должно быть достаточно для тестирования. Давайте посмотрим, как отображается документация по адресу <адрес сервера>/docs/index.html.

Хорошая новость — сайт документации API работает. Плохая новость — он не похож на обычный сайт документации API. Что мы пропустили?

Генерировать документацию swagger каждый раз при изменении строки doc

Документация Swagger хранится в собственной doc-строке Go. У нас есть специальный синтаксис, которому мы следуем. Подробнее об этом позже. Но сначала давайте узнаем, как генерировать документацию.

$ swag init
2022/05/25 23:59:16 Generate swagger docs....
2022/05/25 23:59:16 Generate general API Info, search dir:./
2022/05/25 23:59:16 create docs.go at  docs/docs.go
2022/05/25 23:59:16 create swagger.json at  docs/swagger.json
2022/05/25 23:59:16 create swagger.yaml at  docs/swagger.yaml
Вход в полноэкранный режим Выход из полноэкранного режима

Мы запускаем swag init каждый раз, когда обновляем документацию для нашего API. Это генерирует 3 файла в подкаталоге docs/.

$ tree docs
docs
├── docs.go
├── swagger.json
└── swagger.yaml

0 directories, 3 files
Вход в полноэкранный режим Выход из полноэкранного режима

swagger.json и swagger.yaml — это фактическая спецификация, которую вы можете загрузить на такие сервисы, как AWS API Gateway и подобные. docs.go — это код клея, который нам нужно импортировать в наш сервер.

Давайте теперь импортируем документацию в наш main.go.

 package main

 import (
+       _ "github.com/santosh/gingo/docs"
        "github.com/santosh/gingo/routes"
        swaggerFiles "github.com/swaggo/files"
        ginSwagger "github.com/swaggo/gin-swagger"
 )

+// @title     Gingo Bookstore API
 func main() {
        router := routes.SetupRouter()
Вход в полноэкранный режим Выход из полноэкранного режима

Как вы видите, мы импортировали модуль docs, указав полный путь к модулю. Мы также должны дополнить этот импорт с помощью _, потому что он не используется в файле main.go в явном виде.

Вы также могли заметить строку // @title Gingo Bookstore API. Обратите внимание на ее положение, так как это важно. Она находится прямо над функцией main(). Также @title здесь не случайно. Это одно из ключевых слов, которое документируется в разделе Общая информация об API. По ходу дела мы увидим больше подобных примеров, а пока давайте перегенерируем нашу документацию и просмотрим документацию API.

Похоже, у нас что-то получается. 😀

Добавление общей информации об API на сервер Gin API

В прошлом разделе мы видели аннотацию @title. Она используется для задания заголовка для сервера API. Но это не единственная доступная аннотация. В Swagger существует широкий спектр аннотаций. Вы можете найти их применение в реальной жизни здесь.

Я собираюсь использовать некоторые из них для построения метаданных на моем сайте doc.

// @title           Gin Book Service
// @version         1.0
// @description     A book management service API in Go using Gin framework.
// @termsOfService  https://tos.santoshk.dev

// @contact.name   Santosh Kumar
// @contact.url    https://twitter.com/sntshk
// @contact.email  sntshkmr60@gmail.com

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

// @host      localhost:8080
// @BasePath  /api/v1
Вход в полноэкранный режим Выход из полноэкранного режима
  1. Запустите swag init.
  2. Повторно запустите сервер.
  3. Проверьте наличие обновлений:

Там есть некоторые метаданные.

Вы также могли заметить, что спецификация API внутри каталога docs/ изменилась. Например, вот файл swagger.yaml из каталога docs/.

 {
     "swagger": "2.0",
     "info": {
-        "title": "Gingo Bookstore API",
-        "contact": {}
+        "description": "A book management service API in Go using Gin framework.",
+        "title": "Gin Book Service",
+        "termsOfService": "https://tos.santoshk.dev",
+        "contact": {
+            "name": "Santosh Kumar",
+            "url": "https://twitter.com/sntshk",
+            "email": "sntshkmr60@gmail.com"
+        },
+        "license": {
+            "name": "Apache 2.0",
+            "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
+        },
+        "version": "1.0"
     },
+    "host": "localhost:8080",
+    "basePath": "/api/v1",
     "paths": {}
 }
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь пришло время добавить документацию для конечных точек API.

Добавьте информацию об операциях API к конечным точкам API

Подобно общей информации об API для основного сервера, существует также информация об операциях API для отдельных маршрутов/конечных точек.

Они более разнообразны, чем те, которые я собираюсь использовать здесь. Те, которые я собираюсь использовать, являются лишь их подмножеством. Если вы хотите посмотреть несколько реальных примеров, вы можете найти их здесь на каждом из обработчиков маршрутов.

Вот модифицированный handlers/books.go:

        "github.com/santosh/gingo/models"
 )

-// GetBooks responds with the list of all books as JSON.
+// GetBooks             godoc
+// @Summary      Get books array
+// @Description  Responds with the list of all books as JSON.
+// @Tags         books
+// @Produce      json
+// @Success      200  {array}  models.Book
+// @Router       /books [get]
 func GetBooks(c *gin.Context) {
        c.JSON(http.StatusOK, db.Books)
 }

-// PostBook takes a book JSON and store in DB.
+// PostBook             godoc
+// @Summary      Store a new book
+// @Description  Takes a book JSON and store in DB. Return saved JSON.
+// @Tags         books
+// @Produce      json
+// @Param        book  body      models.Book  true  "Book JSON"
+// @Success      200   {object}  models.Book
+// @Router       /books [post]
 func PostBook(c *gin.Context) {
        var newBook models.Book

@@ -28,7 +41,14 @@ func PostBook(c *gin.Context) {
        c.JSON(http.StatusCreated, newBook)
 }

-// GetBookByISBN locates the book whose ISBN value matches the isbn
+// GetBookByISBN                godoc
+// @Summary      Get single book by isbn
+// @Description  Returns the book whose ISBN value matches the isbn.
+// @Tags         books
+// @Produce      json
+// @Param        isbn  path      string  true  "search book by isbn"
+// @Success      200  {object}  models.Book
+// @Router       /books/{isbn} [get]
 func GetBookByISBN(c *gin.Context) {
        isbn := c.Param("isbn")
Вход в полноэкранный режим Выйти из полноэкранного режима
  1. Запустите swag init.
  2. Повторно запустите сервер.
  3. Проверьте наличие обновлений:

Вот обновленный swagger.yaml:

 basePath: /api/v1
+definitions:
+  models.Book:
+    properties:
+      author:
+        type: string
+      isbn:
+        type: string
+      title:
+        type: string
+    type: object
 host: localhost:8080
 info:
   contact:
@@ -12,5 +22,58 @@ info:
   termsOfService: https://tos.santoshk.dev
   title: Gin Book Service
   version: "1.0"
-paths: {}
+paths:
+  /books:
+    get:
+      description: Responds with the list of all books as JSON.
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/models.Book'
+            type: array
+      summary: Get books array
+      tags:
+      - books
+    post:
+      description: Takes a book JSON and store in DB. Return saved JSON.
+      parameters:
+      - description: Book JSON
+        in: body
+        name: book
+        required: true
+        schema:
+          $ref: '#/definitions/models.Book'
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.Book'
+      summary: Store a new book
+      tags:
+      - books
+  /books/{isbn}:
+    get:
+      description: Returns the book whose ISBN value matches the isbn.
+      parameters:
+      - description: search book by isbn
+        in: path
+        name: isbn
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/models.Book'
+      summary: Get single book by isbn
+      tags:
+      - books
 swagger: "2.0"
Войдите в полноэкранный режим Выход из полноэкранного режима

Обратите внимание на добавление новых разделов definitions и paths. Они предназначены для моделей и конечных точек соответственно.

Бонус: Группа маршрутизаторов для /api/v1

Всегда полезно добавлять к маршрутам api/v1. Бит v1 имеет логическое обоснование. Он предназначен для тех случаев, когда вам нужно внести изменение, которое не имеет обратной совместимости. Возможно, на входе или выходе есть изменения, которые могут сломать миллионы зависимых клиентов.

В таких ситуациях вы увеличиваете версию до чего-то вроде api/v2 и позволяете старому API-серверу обслуживать старые запросы от старого обработчика.

Сейчас все маршруты в gingo server начинаются с /book. Мы собираемся изменить это.

Вот измененный routes/routes.go.


 func SetupRouter() *gin.Engine {
        router := gin.Default()
-       router.GET("/books", handlers.GetBooks)
-       router.GET("/books/:isbn", handlers.GetBookByISBN)
-       // router.DELETE("/books/:isbn", handlers.DeleteBookByISBN)
-       // router.PUT("/books/:isbn", handlers.UpdateBookByISBN)
-       router.POST("/books", handlers.PostBook)
+
+       v1 := router.Group("/api/v1")
+       {
+               v1.GET("/books", handlers.GetBooks)
+               v1.GET("/books/:isbn", handlers.GetBookByISBN)
+               // router.DELETE("/books/:isbn", handlers.DeleteBookByISBN)
+               // router.PUT("/books/:isbn", handlers.UpdateBookByISBN)
+               v1.POST("/books", handlers.PostBook)
+       }

        return router
 }
Вход в полноэкранный режим Выход из полноэкранного режима

Здесь мы используем router.Group для создания группы с путем /api/v1. Затем мы переносим все определения маршрутов в группу и окружаем ее скобками. Вот как мы создаем маршрут пути в Gin.

Заключение

Документация API — это важная часть документации API. Вместо того чтобы документировать конечные точки где-либо еще, мы можем документировать маршруты прямо в коде. Таким образом, у нас будет только один единственный источник правды. Нет необходимости поддерживать код и документацию отдельно. В свою очередь, мы получаем всегда актуальную документацию.

Каждый бэкенд-сервер в той или иной степени поддерживает Swagger UI. Я описал основы для Gin, но если вы используете какой-либо другой фреймворк, я советую вам поискать свой собственный фреймворк.

Оцените статью
Procodings.ru
Добавить комментарий