Как составить swagger

Уровень сложности
Средний

Время на прочтение
41 мин

Количество просмотров 4.6K

В современных реалиях разработки программного обеспечения бывает достаточно трудно быстро и качественно написать техническую документацию к проекту, особенно когда данному процессу уделяется недостаточно времени по разным причинам: от временных ограничений, до индивидуальной непереносимости монотонного процесса участниками проектной группы, который, порой, может тормозить рабочие процессы.

Уже сейчас существуют инструменты, которые позволяют ускорить процесс документирования программного обеспечения. Эти инструменты значительно облегчают задачи разработчиков по ведению технической документации и внесению быстрых правок, что позволяет отражать в документации актуальные положения фактического поведения системы. В данной статье будет рассмотрен один из таких инструментов.

Основное внимание в статье будет уделено автоматизации процесса создания документации API сервисов, которые разрабатываются с помощью фреймворков Express.js и Gin, используя подходящий для этой задачи инструмент – Swagger.

Введение

В качестве основного источника мотивации для написания данной статьи я избрал свой личный опыт разрешения проблем с быстрым и удобным документированием API сервисов, с которыми мне пришлось столкнуться при разработке сервисов на Express.js. Данным опытом я хотел бы поделиться с читателем, так как нахожу его полезным и, возможно, он принесёт пользу разработчикам, которые столкнулись с аналогичными проблемами.

Под сервисом в статье подразумевается любое серверное приложение, которое принимает запросы на определённом порту и возвращает ответ после проведения определённых манипуляций с данными.

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

В конце статьи будут приведены ссылки на исходный код, который использовался в целях продемонстрировать возможности рассматриваемых пакетов и используемых скриптов. Код данных проектов следует рассматривать исключительно как пример, а не полноценно работающие приложения.

Что такое Swagger и OpenAPI?

Swagger – это профессиональный набор инструментов для разработчиков API. Данный набор инструментов активно разрабатывается SmartBear Software и поддерживается сообществом открытого исходного кода (Open Source).

OpenAPI – это спецификация для описания API. На текущий момент времени актуальная версия OpenAPI – 3.1.0.

Swagger использует спецификацию OpenAPI для описания и документирования API, а инструменты Swagger позволяют использовать эту спецификацию для создания и тестирования API, а также для генерации клиентского кода.

Набор инструментов Swagger включает в себя следующие наиболее используемые инструменты:

  1. Swagger Editor – редактор для разработки API-интерфейсов в соответствии со спецификацией Open API

  2. Swagger UI – веб-приложение, позволяющая визуализировать определения спецификаций Open API в интерактивном пользовательском интерфейсе

  3. Swagger Codegen – создание серверных заглушек и клиентских SDK-пакетов на основе определений спецификаций Open API

Этим списком весь набор Swagger не заканчивается, их достаточно много. Читателю предлагается ознакомиться со всем набором инструментов на официальном сайте.

Из вышеприведённого списка наиболее интересным инструментом является Swagger Editor, поскольку он предоставляет интерфейс для создания файла документации по спецификации Open API вручную.

Однако, перед началом рассмотрения инструмента Swagger Editor следует ответить на вопрос, а что же мы, собственно, собираемся документировать? API слишком общее понятие, следует конкретизировать что именно мы собираемся документировать. И в этом может помочь архитектурный паттерн Controller Service Repository.

Controller Service Repository

Controller Service Repository (CSR) – архитектурный паттерн, который помогает разделить ответственность между слоями приложения и соблюдать принципы SOLID.

Само определение паттерна содержит три важных элемента:

  1. Controller – слой классов, отвечающих за обработку запросов (в частности – пользовательских). В данных классах происходит получение запроса от клиента (request), валидация входных данных (если она подразумевалась), передача данных конкретному сервису, обработка ошибок и формирование ответа пользователю (response).

  2. Service – слой классов, отвечающих за бизнес-логику приложения. В сервисах отсутствуют прямые обращения к базе данных – данная функция делегируется репозиториям, которые завершают это перечисление.

  3. Repository – слой классов, отвечающих за сохранение и извлечение некоторого набора данных. В репозиториях не должно быть бизнес-логики. В методах репозитория должны лишь формироваться и выполняться запросы к базе данных.

Удобно представлять эти слои в виде сферы из разных слоёв, каждый из которых по цепочки находится “ближе к данным” (постепенное уменьшение уровня абстракции над взаимодействием с базой данных).

На рисунке ниже представлена данная модель.

Рисунок 1 - Модель архитектурного паттерна CSR

Рисунок 1 – Модель архитектурного паттерна CSR

Причём здесь данный архитектурный паттерн? Дело в том, что данный паттерн является одним из самых популярных на сегодняшний день и отлично подходит для объяснения момента, с которого начинается документирование API (определение этой границы).

Все API, как правило, определяются в объединении множества маршрутов и конкретных обработчиков в слое Controller (иными словами – привязка конкретного метода из любого класса слоя Controller, к конкретному маршруту).

Следовательно, в слое Controller и происходит документирование API. Возможны комбинации, но в основном это можно принять как правило. Это будет рассмотрено на практике более детально.

Swagger Editor

Как уже ранее упоминалось Swagger Editor позволяет визуализировать документацию в соответствии с описанием по спецификации OpenAPI.

Каким образом представлено данное описание? В формате YAML. Грубо говоря вся документация проекта будет представлена в виде одного файла с расширением yaml. Это очень удобно, ведь при необходимости этот документ можно перенести куда угодно, при этом он будет однозначно интерпретирован рассматриваемым набором инструментов, т.к. существует единый стандарт (а в случае проблем с соответствием стандарту инструменты будут выдавать сообщения об ошибках).

На рисунке ниже представлена работа Swigger Editor.

Рисунок 2 - Swagger Editor в действии

Рисунок 2 – Swagger Editor в действии

Данные, представленные в формате YAML, можно изменять и тогда в Swagger Editor будут отображаться все изменения.

Теоретически можно самостоятельно описать все API в своём сервисе с помощью данного инструмента и просто передавать файл с расширением yaml между разработчиками, чтобы они сами у себя запускали веб-приложение Swagger UI и указывали источником данных этот файл, однако данный подход крайне неудобен. Даже сейчас файл документации содержит порядка 800 строк, что довольно громоздко для количества маршрутов и их сложности, представленных в качестве базового примера в Swagger Editor.

Как можно решить проблему с ручным документированием API? Автоматизировать данный процесс. Тут мы приступаем к рассмотрению практической части.

Определение функциональных требований к сервисам

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

Один сервис будет использовать технологический стек Node.js, Express.js, JavaScript, а другой – Gin, Golang. Первый сервис будет иметь кодовое название NEJ, а второй – GG.

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

Приступим к разработке сервиса с кодовым названием NEJ.

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

Для начала изучим файловую структуру проекта.

Рисунок 3 - Файловая структура проекта NEJ

Рисунок 3 – Файловая структура проекта NEJ

В файловой структуре проекта определены следующие директории и файлы (только основные элементы):

  1. config – директория, содержащая конфигурационные файлы сервиса

  2. constants – директория с общими константами

  3. controllers – директория, содержащая контроллеры (слой Controller)

  4. db – директория с настройками подключения к базе данных и определением моделей

  5. docs – документация сервиса (*.yaml)

  6. dtos – директория, содержащая DTO

  7. exceptions – директория, содержащая основные ошибки сервиса (классы для обработки ошибок)

  8. logger – настройки логгера

  9. logs – логи сервиса

  10. middlewares – промежуточное программное обеспечение (используется для проверки токенов JWT и обработки исключений)

  11. routers – директория, содержащая конкретные привязки методов контроллеров с конкретными маршрутами (url-адресами)

  12. services – директория, содержащая сервисы (слой Services)

  13. utils – директория с утилитами

  14. *.env – файлы с переменными окружения

  15. generate-doc.js – скрипт, для автоматической генерации документации API

  16. index.js – точка входа в серверное приложение

  17. wipe-dependencies.js – скрипт, для автоматического обновления пакетов в package.json

Далее, опишем основные элементы данного сервиса, начиная с точки входа и скрипта для автоматического документирования.

Точка входа в NEJ

Код точки входа в серверное приложение выглядит следующим образом:

// Конфигурирование пакета dotenv, для обращения к переменным окружения
import dotenv from 'dotenv';
dotenv.config({ path: `.${process.env.NODE_ENV}.env` });

import express from "express";                                              // Подключение Express.js
import config from "config";                                                // Подключение config, для конфигурирования приложения
import logger from "./logger/logger.js";                                    // Подключение логгера
import cors from "cors";                                                    // Подключение cors'ов
import cookieParser from "cookie-parser";                                   // Подключение cookie-parses
import webApiConfig from "./config/web.api.json" assert { type: "json" };   // Подключение JSON-объекта
import { AuthRouteBase } from './constants/routes/auth.js';                 // Подключение базового маршрута авторизации (корневой route)
import AuthRouter from './routers/auth-routers.js';                         // Подключение роутеров авторизации
import errorMiddleware from './middlewares/error-middleware.js';            // Подключение промежуточного ПО для обработки ошибок
import db from "./db/index.js";                                             // Подключение к базе данных
import { fileURLToPath } from 'url';                                        // Подключение функции для конвертации URL в путь
import path, { dirname } from 'path';                                       // Подключение объекта для работы с путями и функции dirname
import YAML from 'yamljs';                                                  // Подключение объекта, для работы с YAML
import swaggerUi from 'swagger-ui-express';                                 // Подключение пакета swagger-ui-express
import ExpressSwaggerGenerator from 'express-swagger-generator';            // Подключение пакета express-swagger-generator
import swiggerOptions from './config/swigger.options.js';                   // Подключение настроек Swagger'a

// Получаем __dirname
const __dirname = dirname(fileURLToPath(import.meta.url));

// Загрузка файла документации
const swaggerDocument = YAML.load(path.join(__dirname, 'docs', 'docs.yaml'));

// Определение Express-приложения
const app = express();

// Опционально отображаем документацию Swagger версии 2
if (config.get("doc.swagger2") === true) {
    const expressSwaggerGenerator = ExpressSwaggerGenerator(app);
    expressSwaggerGenerator(swiggerOptions(__dirname));
}

app.use(express.json({ extended: true }));
app.use(cookieParser());

// Добавляем по маршруту /docs определённый контроллер (по /docs будет отображаться документация)
app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));

// Настройка cors-политик
app.use(cors({
    credentials: true,
    origin: webApiConfig['web_api'].map((value) => {
        return value;
    })
}));

// Установка маршрутов авторизации
app.use(AuthRouteBase, AuthRouter);

// Установка промежуточного ПО для обработки ошибок
app.use(errorMiddleware);

const PORT = config.get('port') || 5000;

/**
 * Запуск серверного приложения
 * @returns Экземпляр серверного приложения
 */
const start = () => {
    try {
        // Начало прослушивания входящих подключений
        const server = app.listen(PORT, () => console.log(`Сервер запущен с портом ${PORT}`));

        // Запись в логи
        logger.info({
            port: PORT,
            message: "Запуск сервера"
        });

        return server;
    } catch (e) {
        logger.error({
            message: e.message
        });

        process.exit(1);
    }
}

// Запуск сервера
const server = start();

Далее последуют объяснения кода из точки входа в NEJ.

Сперва происходит загрузка файла документации в формате YAML в переменную swaggerDocument

// Загрузка файла документации
const swaggerDocument = YAML.load(path.join(__dirname, 'docs', 'docs.yaml'));

Это даёт нам возможность визуализировать документацию с помощью пакета swagger-ui-express. Перейдём к рассмотрению привязки данного файла к Swagger UI

// Добавляем по маршруту /docs определённый контроллер (по /docs будет отображаться документация)
app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));

На данном коде происходит привязка Swagger UI к маршруту “/docs”. Когда пользователь переходит по этому маршруту, будет показан интерфейс Swagger UI с предоставленной документацией, которая описывает активности и доступные ресурсы API этого сервиса. Swagger UI обеспечивает удобное взаимодействие с API, позволяя отправлять запросы и просматривать ответы в реальном времени.

На рисунке ниже представлена работа по данному маршруту Swagger UI.

Рисунок 4 - Документация проекта NEJ со спецификацией OpenAPI 3

Рисунок 4 – Документация проекта NEJ со спецификацией OpenAPI 3

Как видно из рисунка всё действительно работает, более того – отображается документация в спецификации OpenAPI 3.

Внимательный читатель может отметить в коде точки входа, что есть отдельный опциональный блок, который позволяет увидеть документацию в более ранней спецификации (OpenAPI 2).

Однако, зачем это необходимо? Ответ прост – демонстрация исходной документации Swagger, которая была первоначально сгенерирована. Дело в том, что документация по умолчанию генерируется по спецификации OpenAPI 2, т.к. это особенности используемого пакета.

Для автоматической генерации документации по контроллерам был использован пакет express-swagger-generator, который не пользуется большой популярностью в статьях подобного характера (Swagger, Express.js).

Данный пакет отлично справился с задачей автоматической генерации документации. Ссылка на официальную документацию.

Рисунок 5 - Документация проекта NEJ со спецификацией OpenAPI 2

Рисунок 5 – Документация проекта NEJ со спецификацией OpenAPI 2

Приступим к обзору скрипта, который производит автоматическую генерацию документации в спецификацию OpenAPI 2, а затем конвертирует её в спецификацию OpenAPI 3.

Скрипт автоматической генерации документации API

Ниже представлен скрипт автоматической генерации документации API

import express from "express";                                      // Подключение Express.js (формальность)
import ExpressSwaggerGenerator from 'express-swagger-generator';    // Подключение пакета для автоматического генерирования документации
import swiggerOptions from './config/swigger.options.js';           // Подключение опций для Swagger
import { fileURLToPath } from 'url';                                
import { dirname } from 'path';
const __dirname = dirname(fileURLToPath(import.meta.url));
import jsonToYaml from 'json2yaml';                                 // Подключение конвертера из JSON в YAML
import fs from 'fs';
import swaggerConverter from 'swagger2openapi';                     // Подключение конвертера документации Swagger2 в OpenAPI 3

// Привязка генератора к конкретному экземпляру приложения Express
const expressSwaggerGenerator = ExpressSwaggerGenerator(express());

// Генерирование документации по определённым настройкам
const swaggerDoc = expressSwaggerGenerator(swiggerOptions(__dirname));

// Синхронная запись данных в файл документации
fs.writeFileSync('./docs/docs_swagger2.yaml', jsonToYaml.stringify(swaggerDoc));

// Процесс конвертации документации в формате Swagger 2 в документацию формата OpenAPI 3
swaggerConverter.convertObj(swaggerDoc, {}, (err, options) => {
    if (err) {
        console.error(err);
    } else {
        // Конвертация JSON в YAML
        const output = jsonToYaml.stringify(options.openapi);

        // Запись результата конвертации документации в файл (он в дальнейшем и используется по умолчанию для вывода документации)
        fs.writeFileSync('./docs/docs.yaml', output);
        process.exit(0);
    }
});

Немного разъясним работу данного скрипта. Он запускается через отдельный скрипт в package.json, а именно – с помощью npm run generate:doc

"scripts": {
    "start": "cross-env NODE_ENV=production nodemon index.js",
    "start:dev": "nodemon index.js",
    "dev": "cross-env NODE_ENV=development concurrently "npm run start:dev"",
    "generate:doc": "node generate-doc.js",
    "__comment upd pkg__": "Скрипт запускающий процесс обновления пакетов",
    "update:packages:windows": "node wipe-dependencies.js && rd /s node_modules && npm update --save-dev && npm update --save",
    "update:packages:linux": "node wipe-dependencies.js && rm -r node_modules && npm update --save-dev && npm update --save"
  }

После генерации документации в файл docs_swagger2.yaml добавляется сгенерированные данные в формате yaml, согласно спецификации OpenAPI 2

Содержимое файла docs_swagger2.yaml

---
info:
  description: "Данный сервис определяет основные пользовательские функции"
  title: "Основной игровой сервис"
  version: "1.0.0"
  contact:
    email: "swdaniel@yandex.ru"
host: "localhost:5000"
basePath: "/"
produces:
  - "application/json"
  - "application/xml"
schemes:
  - "http"
  - "https"
securityDefinitions:
  JWT:
    type: "apiKey"
    in: "header"
    name: "Authorization"
    description: ""
externalDocs:
  description: "Ссылка на внешнюю документацию"
  url: "http://localhost:5000/api-docs"
swagger: "2.0"
paths:
  /auth/sign-up:
    post:
      parameters:
        - name: "input"
          in: "body"
          description: "Входные данные"
          required: true
          schema:
            $ref: "#/definitions/SignUpDto"
      description: "Регистрация пользователя"
      tags:
        - "Авторизация (пользователь)"
      responses:
        200:
          description: "Авторизационные данные пользователя"
          schema:
            $ref: "#/definitions/AuthDto"
        default:
          description: "Ошибка запроса"
          schema:
            $ref: "#/definitions/ApiError"
  /auth/sign-in:
    post:
      parameters:
        - name: "input"
          in: "body"
          description: "Входные данные"
          required: true
          schema:
            $ref: "#/definitions/SignInDto"
      description: "Авторизация пользователя"
      tags:
        - "Авторизация (пользователь)"
      responses:
        200:
          description: "Авторизационные данные пользователя"
          schema:
            $ref: "#/definitions/AuthDto"
        default:
          description: "Ошибка запроса"
          schema:
            $ref: "#/definitions/ApiError"
  /auth/logout:
    post:
      parameters:
        - name: "input"
          in: "body"
          description: "Входные данные"
          required: true
          schema:
            $ref: "#/definitions/LogoutDto"
      description: "Выход пользователя из системы"
      tags:
        - "Авторизация (пользователь)"
      responses:
        200:
          description: "Флаг, определяющий успех операции выхода пользователя из системы"
          schema:
            $ref: "#/definitions/SuccessDto"
        default:
          description: "Ошибка запроса"
          schema:
            $ref: "#/definitions/ApiError"
  /auth/management/sign-in:
    post:
      parameters:
        - name: "input"
          in: "body"
          description: "Входные данные"
          required: true
          schema:
            $ref: "#/definitions/SignInDto"
      description: "Авторизация пользователя"
      tags:
        - "Авторизация (для управляющего сайта)"
      responses:
        200:
          description: "Авторизационные данные пользователя"
          schema:
            $ref: "#/definitions/AuthDto"
        default:
          description: "Ошибка запроса"
          schema:
            $ref: "#/definitions/ApiError"
  /auth/management/logout:
    post:
      parameters:
        - name: "input"
          in: "body"
          description: "Входные данные"
          required: true
          schema:
            $ref: "#/definitions/LogoutDto"
      description: "Авторизация пользователя"
      tags:
        - "Авторизация (для управляющего сайта)"
      responses:
        200:
          description: "Флаг, определяющий успех операции выхода пользователя из системы"
          schema:
            $ref: "#/definitions/SuccessDto"
        default:
          description: "Ошибка запроса"
          schema:
            $ref: "#/definitions/ApiError"
  /auth/activate:
    post:
      parameters:
        - name: "input"
          in: "body"
          description: "Входные данные"
          required: true
          schema:
            $ref: "#/definitions/ActivationLinkDto"
      description: "Выход пользователя из системы"
      tags:
        - "Авторизация (пользователь)"
      responses:
        200:
          description: "Флаг, определяющий успех операции подтверждения пользователя"
          schema:
            $ref: "#/definitions/SuccessDto"
        default:
          description: "Ошибка запроса"
          schema:
            $ref: "#/definitions/ApiError"
  /auth/refresh/token:
    post:
      parameters:
        - name: "input"
          in: "body"
          description: "Входные данные"
          required: true
          schema:
            $ref: "#/definitions/RefreshDto"
      description: "Выход пользователя из системы"
      tags:
        - "Авторизация (пользователь)"
      responses:
        200:
          description: "Авторизационные данные пользователя"
          schema:
            $ref: "#/definitions/AuthDto"
        default:
          description: "Ошибка запроса"
          schema:
            $ref: "#/definitions/ApiError"
definitions:
  ActivationLinkDto:
    required:
      - "activation_link"
    properties:
      activation_link:
        type: "string"
        description: ""
  AttributeDto:
    required:
      - "read"
      - "write"
      - "update"
      - "delete"
    properties:
      read:
        type: "boolean"
        description: ""
      write:
        type: "boolean"
        description: ""
      update:
        type: "boolean"
        description: ""
      delete:
        type: "boolean"
        description: ""
  AuthDto:
    required:
      - "tokens"
      - "users_id"
      - "type_auth"
      - "refresh_token"
      - "attributes"
    properties:
      tokens:
        $ref: "#/definitions/TokenDto"
      users_id:
        type: "number"
        description: ""
      type_auth:
        type: "number"
        description: ""
      refresh_token:
        $ref: "#/definitions/ModuleDto"
      attributes:
        $ref: "#/definitions/AttributeDto"
  LogoutDto:
    required:
      - "users_id"
      - "access_token"
      - "refresh_token"
      - "type_auth"
    properties:
      users_id:
        type: "number"
        description: ""
      access_token:
        type: "string"
        description: ""
      refresh_token:
        type: "string"
        description: ""
      type_auth:
        type: "number"
        description: ""
  ModuleDto:
    required:
      - "player"
      - "judge"
      - "creator"
      - "moderator"
      - "manager"
      - "admin"
      - "super_admin"
    properties:
      player:
        type: "boolean"
        description: ""
      judge:
        type: "boolean"
        description: ""
      creator:
        type: "boolean"
        description: ""
      moderator:
        type: "boolean"
        description: ""
      manager:
        type: "boolean"
        description: ""
      admin:
        type: "boolean"
        description: ""
      super_admin:
        type: "boolean"
        description: ""
  RefreshDto:
    required:
      - "refresh_token"
      - "type_auth"
    properties:
      refresh_token:
        type: "string"
        description: ""
      type_auth:
        type: "number"
        description: ""
  SignInDto:
    required:
      - "email"
      - "password"
    properties:
      email:
        type: "string"
        description: ""
      password:
        type: "string"
        description: ""
  SignUpDto:
    required:
      - "email"
      - "password"
      - "phone_num"
      - "location"
      - "date_birthday"
      - "nickname"
      - "name"
      - "surname"
    properties:
      email:
        type: "string"
        description: ""
      password:
        type: "string"
        description: ""
      phone_num:
        type: "string"
        description: ""
      location:
        type: "string"
        description: ""
      date_birthday:
        type: "string"
        description: ""
      nickname:
        type: "string"
        description: ""
      name:
        type: "string"
        description: ""
      surname:
        type: "string"
        description: ""
  TokenDto:
    required:
      - "access_token"
      - "refresh_token"
    properties:
      access_token:
        type: "string"
        description: ""
      refresh_token:
        type: "string"
        description: ""
  SuccessDto:
    required:
      - "success"
    properties:
      success:
        type: "boolean"
        description: ""
  ApiError:
    required:
      - "message"
      - "errors"
    properties:
      message:
        type: "string"
        description: ""
      errors:
        type: "array"
        items:
          $ref: "#/definitions/FieldError"
  FieldError:
    required:
      - "type"
      - "value"
      - "msg"
      - "path"
      - "location"
    properties:
      type:
        type: "string"
        description: ""
      value:
        type: "string"
        description: ""
      msg:
        type: "string"
        description: ""
      path:
        type: "string"
        description: ""
      location:
        type: "string"
        description: ""
responses: {}
parameters: {}
tags:
  - name: "Авторизация (пользователь)"
    description: "Функции для авторизации пользователя"
  - name: "Авторизация (для управляющего сайта)"
    description: "Функция для авторизации пользователя"

Также в скрипте идёт процесс конвертации файла с спецификацией OpenAPI 2, в файл с спецификацией OpenAPI 3

// Процесс конвертации документации в формате Swagger 2 в документацию формата OpenAPI 3
swaggerConverter.convertObj(swaggerDoc, {}, (err, options) => {
    if (err) {
        console.error(err);
    } else {
        // Конвертация JSON в YAML
        const output = jsonToYaml.stringify(options.openapi);

        // Запись результата конвертации документации в файл (он в дальнейшем и используется по умолчанию для вывода документации)
        fs.writeFileSync('./docs/docs.yaml', output);
        process.exit(0);
    }
});

Соответственно после конвертации появится файл docs.yaml, в котором будет содержимое уже в спецификации OpenAPI 3.0

Содержимое файла docs.yaml

---
openapi: "3.0.0"
info:
  description: "Данный сервис определяет основные пользовательские функции"
  title: "Основной игровой сервис"
  version: "1.0.0"
  contact:
    email: "swdaniel@yandex.ru"
externalDocs:
  description: "Ссылка на внешнюю документацию"
  url: "http://localhost:5000/api-docs"
paths:
  /auth/sign-up:
    post:
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SignUpDto"
        description: "Входные данные"
        required: true
      description: "Регистрация пользователя"
      tags:
        - "Авторизация (пользователь)"
      responses:
        200:
          description: "Авторизационные данные пользователя"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthDto"
            application/xml:
              schema:
                $ref: "#/components/schemas/AuthDto"
        default:
          description: "Ошибка запроса"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
            application/xml:
              schema:
                $ref: "#/components/schemas/ApiError"
  /auth/sign-in:
    post:
      requestBody:
        $ref: "#/components/requestBodies/SignInDto"
      description: "Авторизация пользователя"
      tags:
        - "Авторизация (пользователь)"
      responses:
        200:
          description: "Авторизационные данные пользователя"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthDto"
            application/xml:
              schema:
                $ref: "#/components/schemas/AuthDto"
        default:
          description: "Ошибка запроса"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
            application/xml:
              schema:
                $ref: "#/components/schemas/ApiError"
  /auth/logout:
    post:
      requestBody:
        $ref: "#/components/requestBodies/LogoutDto"
      description: "Выход пользователя из системы"
      tags:
        - "Авторизация (пользователь)"
      responses:
        200:
          description: "Флаг, определяющий успех операции выхода пользователя из системы"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessDto"
            application/xml:
              schema:
                $ref: "#/components/schemas/SuccessDto"
        default:
          description: "Ошибка запроса"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
            application/xml:
              schema:
                $ref: "#/components/schemas/ApiError"
  /auth/management/sign-in:
    post:
      requestBody:
        $ref: "#/components/requestBodies/SignInDto"
      description: "Авторизация пользователя"
      tags:
        - "Авторизация (для управляющего сайта)"
      responses:
        200:
          description: "Авторизационные данные пользователя"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthDto"
            application/xml:
              schema:
                $ref: "#/components/schemas/AuthDto"
        default:
          description: "Ошибка запроса"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
            application/xml:
              schema:
                $ref: "#/components/schemas/ApiError"
  /auth/management/logout:
    post:
      requestBody:
        $ref: "#/components/requestBodies/LogoutDto"
      description: "Авторизация пользователя"
      tags:
        - "Авторизация (для управляющего сайта)"
      responses:
        200:
          description: "Флаг, определяющий успех операции выхода пользователя из системы"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessDto"
            application/xml:
              schema:
                $ref: "#/components/schemas/SuccessDto"
        default:
          description: "Ошибка запроса"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
            application/xml:
              schema:
                $ref: "#/components/schemas/ApiError"
  /auth/activate:
    post:
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ActivationLinkDto"
        description: "Входные данные"
        required: true
      description: "Выход пользователя из системы"
      tags:
        - "Авторизация (пользователь)"
      responses:
        200:
          description: "Флаг, определяющий успех операции подтверждения пользователя"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessDto"
            application/xml:
              schema:
                $ref: "#/components/schemas/SuccessDto"
        default:
          description: "Ошибка запроса"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
            application/xml:
              schema:
                $ref: "#/components/schemas/ApiError"
  /auth/refresh/token:
    post:
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RefreshDto"
        description: "Входные данные"
        required: true
      description: "Выход пользователя из системы"
      tags:
        - "Авторизация (пользователь)"
      responses:
        200:
          description: "Авторизационные данные пользователя"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthDto"
            application/xml:
              schema:
                $ref: "#/components/schemas/AuthDto"
        default:
          description: "Ошибка запроса"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
            application/xml:
              schema:
                $ref: "#/components/schemas/ApiError"
tags:
  - name: "Авторизация (пользователь)"
    description: "Функции для авторизации пользователя"
  - name: "Авторизация (для управляющего сайта)"
    description: "Функция для авторизации пользователя"
servers:
  - url: "http://localhost:5000"
  - url: "https://localhost:5000"
components:
  requestBodies:
    SignInDto:
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/SignInDto"
      description: "Входные данные"
      required: true
    LogoutDto:
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/LogoutDto"
      description: "Входные данные"
      required: true
  securitySchemes:
    JWT:
      type: "apiKey"
      in: "header"
      name: "Authorization"
      description: ""
  schemas:
    ActivationLinkDto:
      required:
        - "activation_link"
      properties:
        activation_link:
          type: "string"
          description: ""
    AttributeDto:
      required:
        - "read"
        - "write"
        - "update"
        - "delete"
      properties:
        read:
          type: "boolean"
          description: ""
        write:
          type: "boolean"
          description: ""
        update:
          type: "boolean"
          description: ""
        delete:
          type: "boolean"
          description: ""
    AuthDto:
      required:
        - "tokens"
        - "users_id"
        - "type_auth"
        - "refresh_token"
        - "attributes"
      properties:
        tokens:
          $ref: "#/components/schemas/TokenDto"
        users_id:
          type: "number"
          description: ""
        type_auth:
          type: "number"
          description: ""
        refresh_token:
          $ref: "#/components/schemas/ModuleDto"
        attributes:
          $ref: "#/components/schemas/AttributeDto"
    LogoutDto:
      required:
        - "users_id"
        - "access_token"
        - "refresh_token"
        - "type_auth"
      properties:
        users_id:
          type: "number"
          description: ""
        access_token:
          type: "string"
          description: ""
        refresh_token:
          type: "string"
          description: ""
        type_auth:
          type: "number"
          description: ""
    ModuleDto:
      required:
        - "player"
        - "judge"
        - "creator"
        - "moderator"
        - "manager"
        - "admin"
        - "super_admin"
      properties:
        player:
          type: "boolean"
          description: ""
        judge:
          type: "boolean"
          description: ""
        creator:
          type: "boolean"
          description: ""
        moderator:
          type: "boolean"
          description: ""
        manager:
          type: "boolean"
          description: ""
        admin:
          type: "boolean"
          description: ""
        super_admin:
          type: "boolean"
          description: ""
    RefreshDto:
      required:
        - "refresh_token"
        - "type_auth"
      properties:
        refresh_token:
          type: "string"
          description: ""
        type_auth:
          type: "number"
          description: ""
    SignInDto:
      required:
        - "email"
        - "password"
      properties:
        email:
          type: "string"
          description: ""
        password:
          type: "string"
          description: ""
    SignUpDto:
      required:
        - "email"
        - "password"
        - "phone_num"
        - "location"
        - "date_birthday"
        - "nickname"
        - "name"
        - "surname"
      properties:
        email:
          type: "string"
          description: ""
        password:
          type: "string"
          description: ""
        phone_num:
          type: "string"
          description: ""
        location:
          type: "string"
          description: ""
        date_birthday:
          type: "string"
          description: ""
        nickname:
          type: "string"
          description: ""
        name:
          type: "string"
          description: ""
        surname:
          type: "string"
          description: ""
    TokenDto:
      required:
        - "access_token"
        - "refresh_token"
      properties:
        access_token:
          type: "string"
          description: ""
        refresh_token:
          type: "string"
          description: ""
    SuccessDto:
      required:
        - "success"
      properties:
        success:
          type: "boolean"
          description: ""
    ApiError:
      required:
        - "message"
        - "errors"
      properties:
        message:
          type: "string"
          description: ""
        errors:
          type: "array"
          items:
            $ref: "#/components/schemas/FieldError"
    FieldError:
      required:
        - "type"
        - "value"
        - "msg"
        - "path"
        - "location"
      properties:
        type:
          type: "string"
          description: ""
        value:
          type: "string"
          description: ""
        msg:
          type: "string"
          description: ""
        path:
          type: "string"
          description: ""
        location:
          type: "string"
          description: ""

Чтобы убедиться в работоспособности данной документации можно вставить содержимое файла docs.yaml в Swagger Editor

Рисунок 6 - Демонстрация факта корректной генерации документации API

Рисунок 6 – Демонстрация факта корректной генерации документации API

Используемые пакеты

Все используемые пакеты в сервисе NEJ представлены в удалённом репозитории.

Содержимое файла package.json

{
  "name": "express-swagger",
  "version": "0.0.1",
  "description": "Основной сервер",
  "main": "index.js",
  "type": "module",
  "author": {
    "name": "Solopov Daniil <swdaniel@yandex.ru>"
  },
  "scripts": {
    "start": "cross-env NODE_ENV=production nodemon index.js",
    "start:dev": "nodemon index.js",
    "dev": "cross-env NODE_ENV=development concurrently "npm run start:dev"",
    "generate:doc": "node generate-doc.js",
    "__comment upd pkg__": "Скрипт запускающий процесс обновления пакетов",
    "update:packages:windows": "node wipe-dependencies.js && rd /s node_modules && npm update --save-dev && npm update --save",
    "update:packages:linux": "node wipe-dependencies.js && rm -r node_modules && npm update --save-dev && npm update --save"
  },
  "keywords": [
    "postgresql",
    "react",
    "nodejs",
    "express"
  ],
  "license": "ISC",
  "dependencies": {
    "bcryptjs": "^2.4.3",
    "config": "^3.3.9",
    "cookie-parser": "^1.4.6",
    "cors": "^2.8.5",
    "dotenv": "^16.0.3",
    "express": "^4.18.2",
    "express-validator": "^7.0.1",
    "googleapis": "^118.0.0",
    "json2yaml": "^1.1.0",
    "jsonwebtoken": "^9.0.0",
    "node-fetch": "^3.3.1",
    "node-geocoder": "^4.2.0",
    "node-unique-id-generator": "^0.1.0",
    "nodemailer": "^6.9.1",
    "pg": "^8.10.0",
    "pg-hstore": "^2.3.4",
    "sequelize": "^6.31.0",
    "socket.io": "^4.6.1",
    "swagger-jsdoc": "^6.2.8",
    "swagger-ui-express": "^4.6.3",
    "swagger2openapi": "^7.0.8",
    "uuid": "^9.0.0",
    "winston": "^3.8.2",
    "yamljs": "^0.3.0"
  },
  "devDependencies": {
    "concurrently": "^8.0.1",
    "cross-env": "^7.0.3",
    "express-swagger-generator": "^1.1.17",
    "nodemon": "^2.0.22",
    "oas3-tools": "^2.2.3",
    "sequelize-cli": "^6.6.0",
    "swagger-autogen": "^2.23.1"
  }
}

Перейдём к функциональным особенностям пакета express-swagger-generator.

Настройки для Swagger’a

Все настройки для Swagger’а, которые используются при генерации документации по спецификации OpenAPI 2 представлены в файле /config/swagger.options.js

// Функция для формирования настроек генерации документации
const options = (dirname) => {
    return {
        // Определения для Swagger'a
        swaggerDefinition: {
            // Блок информации
            info: {
                description: 'Данный сервис определяет основные пользовательские функции',  // Описание
                title: 'Основной игровой сервис',                                           // Название
                version: '1.0.0',                                                           // Версия
                contact: {                                                                  // Контакты
                    email: "swdaniel@yandex.ru"
                }
            },
            host: 'localhost:5000',                                                         // Основной хост
            basePath: '/',                                                                  // Базовый путь
            produces: [                                                                     
                "application/json",
                "application/xml"
            ],
            schemes: ['http', 'https'],
            securityDefinitions: {                                                          // Определения безопасности
                JWT: {
                    type: 'apiKey',
                    in: 'header',
                    name: 'Authorization',
                    description: "",
                }
            },
            externalDocs: {                                                                 // Ссылка на внешнюю документацию
                description: 'Ссылка на внешнюю документацию',
                url: 'http://localhost:5000/api-docs'
            },
        },
        // Маршрут, по которому будет доступна документация в браузере
        route: {
            url: '/docs/swagger2',
            docs: '/swagger.json',
        },
        basedir: dirname,
        // Файлы, которые будут просматриваться генератором и которые будут влиять на конечный результат
        files: ['./routers/*.js', './dtos/**/*.js', './models/**/*.js', './exceptions/*.js']
    };
}

export default options;

С помощью options подключаемый модуль express-swagger-generator понимает, каким образом генерировать документацию, какие файлы учитывать (по каком файлам делать обход), по какому маршруту нужно выводить документацию, определяет основную информацию на странице документации и так далее.

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

Ранее мы уже определили, что документирование API начинается с контроллеров, а если быть точнее – роутеров, которые связывают конкретные адреса с контроллерами. Пора продемонстрировать каким образом пакет express-swagger-generator помогает составить документацию, используя классический JSDoc.

Разберём документирование API на примере POST-запроса на регистрацию нового пользователя.

/**
 * Регистрация пользователя
 * @route POST /auth/sign-up
 * @group Авторизация (пользователь) - Функции для авторизации пользователя
 * @param {SignUpDto.model} input.body.required Входные данные
 * @returns {AuthDto.model} 200 - Авторизационные данные пользователя
 * @returns {ApiError.model} default - Ошибка запроса
 */
router.post(
    AuthRoute.signUp, // Константа конкретного адреса
    [
        // Валидация входных данных
        check('email', 'Введите корректный email').isEmail(),
        check('password', 'Минимальная длина пароля должна быть 6 символов, а максимальная длина пароля - 32 символа')
            .isLength({ min: 6, max: 32 }),
        check('phone_num', 'Некорректный номер телефона').isMobilePhone("ru-RU"),
        check('location', 'Максимальная длина местоположение не может быть меньше 3 символов')
            .isLength({ min: 3 }),
        check('date_birthday', "Некорректная дата рождения").isDate({
            format: "YYYY-MM-DD"
        }),
        check('nickname', 'Минимальная длина для никнейма равна 2 символам')
            .isLength({ min: 2 }),
        check('name', 'Минимальная длина для имени равна 2 символам')
            .isLength({ min: 2 }),
        check('surname', 'Минимальная длина для фамилии равна 2 символам')
            .isLength({ min: 2 })
    ],
    authController.signUp // Конкретный метод контроллера
);

Можно заметить, что описание для данного API представлено в виде последовательности комментариев:

/**
 * Регистрация пользователя
 * @route POST /auth/sign-up
 * @group Авторизация (пользователь) - Функции для авторизации пользователя
 * @param {SignUpDto.model} input.body.required Входные данные
 * @returns {AuthDto.model} 200 - Авторизационные данные пользователя
 * @returns {ApiError.model} default - Ошибка запроса
 */

Эти комментарии по структуре похожи на JSDoc-комментарии, однако их интерпретация используемым пакетом осуществляется по своему.

Дадим разъяснения данным комментариям:

  1. В первой строке многострочного комментария представлено описание конкретного API

  2. @ route POST /auth/sign-up – привязка API к текущему адресу в документации

  3. @ group Авторизация … – соотнесение данного API к конкретной группе (аналогично, что и tag)

  4. @ param {SignUpDto.model} input.body.required – определение модели входных данных (без необходимости в ссылках #ref, по спецификации OpenAPI)

  5. @ returns {AuthDto.model} 200 – описание модели выходных данных (при успешной обработке запроса)

  6. @ return {ApiError.model} default – обработка всех ошибок по умолчанию

Каким образом формируются модели? Приведу пример с моделью SignUpDto:

/**
 * @typedef SignUpDto
 * @property {string} email.required
 * @property {string} password.required
 * @property {string} phone_num.required
 * @property {string} location.required
 * @property {string} date_birthday.required
 * @property {string} nickname.required
 * @property {string} name.required
 * @property {string} surname.required
 */
class SignUpDto {
    email;          // Email-адрес
    password;       // Пароль
    phone_num;      // Номер телефона
    location;       // Локация
    date_birthday;  // День рождения
    nickname;       // Никнейм
    name;           // Имя
    surname;        // Фамилия

    constructor(model) {
        this.email = model.email;
        this.password = model.password;
        this.phone_num = model.phone_num;
        this.location = model.location;
        this.date_birthday = model.date_birthday;
        this.nickname = model.nickname;
        this.name = model.name;
        this.surname = model.surname;
    }
}

export default SignUpDto;

Интересует именно описание в многострочном комментарии:

/**
 * @typedef SignUpDto
 * @property {string} email.required
 * @property {string} password.required
 * @property {string} phone_num.required
 * @property {string} location.required
 * @property {string} date_birthday.required
 * @property {string} nickname.required
 * @property {string} name.required
 * @property {string} surname.required
 */

Дадим пояснение данному определению:

  1. @ typedef SignUpDto – определение модели (схемы) SignUpDto (на которую потом можно будет ссылаться)

  2. @ property {string} email.required – определение параметра email, с типом string

  3. Остальное – по аналогии со вторым элементом из списка

Также можно сделать более сложное определение:

import TokenDto from "./token-dto.js";
import ModuleDto from "./module-dto.js";
import AttributeDto from "./attribute-dto.js";

/**
 * @typedef AuthDto
 * @property {TokenDto.model} tokens.required
 * @property {number} users_id.required
 * @property {number} type_auth.required
 * @property {ModuleDto.model} refresh_token.required
 * @property {AttributeDto.model} attributes.required
 */
class AuthDto {
    tokens;         // Токены
    users_id;       // Идентификатор пользователя
    type_auth;      // Тип авторизации
    modules;        // Модель
    attributes;     // Атрибуты

    constructor(model){
        this.tokens = model.tokens;
        this.users_id = model.tokens;
        this.type_auth = model.type_auth;
        this.modules = model.modules;
        this.attributes = model.attributes;
    }
}

export default AuthDto;

Дадим пояснения тому, что есть в многострочном комментарии:

  1. @ typedef AuthDto – создание модели AuthDto

  2. @ property {TokenDto.model} tokens.required – создание параметра в модели, которое по сути является другой моделью (вложенность схем)

  3. Далее – по аналогии

Необязательно многострочные комментарии с определением моделей держать рядом с моделями, можно держать их и отдельно. Например, следующее определение будет работать корректно:

/**
 * @typedef SignUpDto
 * @property {string} email.required
 * @property {string} password.required
 * @property {string} phone_num.required
 * @property {string} location.required
 * @property {string} date_birthday.required
 * @property {string} nickname.required
 * @property {string} name.required
 * @property {string} surname.required
 */

/**
 * Регистрация пользователя
 * @route POST /auth/sign-up
 * @group Авторизация (пользователь) - Функции для авторизации пользователя
 * @param {SignUpDto.model} input.body.required Входные данные (всё ок)
 * @returns {AuthDto.model} 200 - Авторизационные данные пользователя
 * @returns {ApiError.model} default - Ошибка запроса
 */
router.post(
    AuthRoute.signUp, // Константа конкретного адреса
    [
        // Валидация входных данных
        check('email', 'Введите корректный email').isEmail(),
        check('password', 'Минимальная длина пароля должна быть 6 символов, а максимальная длина пароля - 32 символа')
            .isLength({ min: 6, max: 32 }),
        check('phone_num', 'Некорректный номер телефона').isMobilePhone("ru-RU"),
        check('location', 'Максимальная длина местоположение не может быть меньше 3 символов')
            .isLength({ min: 3 }),
        check('date_birthday', "Некорректная дата рождения").isDate({
            format: "YYYY-MM-DD"
        }),
        check('nickname', 'Минимальная длина для никнейма равна 2 символам')
            .isLength({ min: 2 }),
        check('name', 'Минимальная длина для имени равна 2 символам')
            .isLength({ min: 2 }),
        check('surname', 'Минимальная длина для фамилии равна 2 символам')
            .isLength({ min: 2 })
    ],
    authController.signUp // Конкретный метод контроллера
);

Однако, рекомендуется держать определение моделей в многострочных комментариях держать с их фактическим местоположением, ради достижения модульности, ведь в настройках этот момент учтён.

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

Изучим файловую структуру проекта GG

Рисунок 7 - Файловая структура проекта GG

Рисунок 7 – Файловая структура проекта GG

Приведу пояснения по файловой структуре проекта (только основные элементы):

  1. cmd – директория, в которой определена точка входа в серверное приложение (main.go)

  2. config – директория с файлами конфигурации

  3. database – директория, в которой содержатся схемы и дампы базы данных

  4. docs – директория, в которой располагается сгенерированная документация

  5. logs – логи сервера

  6. pkg – основные пакеты сервиса

  7. server.go – определение сервера

Точка входа в GG

Код точки входа в серверное приложение GG выглядит следующим образом:

package main

import (
	"context"
	"fmt"
	mainserver "main-server"
	"main-server/config"
	initConfigure "main-server/config"
	handler "main-server/pkg/handler"
	repository "main-server/pkg/repository"
	"main-server/pkg/service"
	"os"
	"os/signal"
	"syscall"

	"github.com/casbin/casbin/v2"
	gormadapter "github.com/casbin/gorm-adapter/v3"
	"github.com/joho/godotenv"
	_ "github.com/lib/pq"
	"github.com/sirupsen/logrus"
	"github.com/spf13/viper"
	"gorm.io/driver/postgres"
	"gorm.io/gorm"
)

// @title Основной сервис
// @version 1.0
// description Основной сервис

// @host localhost:5000
// @BasePath /

// @securityDefinitions.apikey ApiKeyAuth
// @in header
// @name Authorization

func main() {

	// Инициализация конфигурации сервера
	if err := initConfig(); err != nil {
		logrus.Fatalf("error initializing configs: %s", err.Error())
	}

	// Инициализация переменных внешней среды
	if err := godotenv.Load(); err != nil {
		logrus.Fatalf("error loading env variable: %s", err.Error())
	}

	// Инициализация логгера
	openLogFiles, err := initConfigure.InitLogrus()
	if err != nil {
		logrus.Error("Ошибка при настройке параметров логгера. Вывод всех ошибок будет осуществлён в консоль")
	}

	// Закрытие всех открытых файлов в результате настройки логгера
	defer func() {
		for _, item := range openLogFiles {
			item.Close()
		}
	}()

	// Создание нового подключения к БД
	db, err := repository.NewPostgresDB(repository.Config{
		Host:     viper.GetString("db.host"),
		Port:     viper.GetString("db.port"),
		Username: viper.GetString("db.username"),
		DBName:   viper.GetString("db.dbname"),
		SSLMode:  viper.GetString("db.sslmode"),
		Password: os.Getenv("DB_PASSWORD"),
	})

	// Создание строки DNS
	dns := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=%s",
		viper.GetString("db.host"),
		viper.GetString("db.username"),
		os.Getenv("DB_PASSWORD"),
		viper.GetString("db.dbname"),
		viper.GetString("db.port"),
		viper.GetString("db.sslmode"),
	)

	// Получение адаптера после открытия подключения к базе данных через gorm
	dbAdapter, err := gorm.Open(postgres.New(postgres.Config{
		DSN: dns,
	}), &gorm.Config{})

	// Создание нового адаптера c кастомной таблицей
	adapter, err := gormadapter.NewAdapterByDBWithCustomTable(dbAdapter, &config.AcRule{}, viper.GetString("rules_table_name"))

	if err != nil {
		logrus.Fatalf("failed to initialize adapter by db with custom table: %s", err.Error())
	}

	// Определение нового объекта enforcer, по модели PERM
	enforcer, err := casbin.NewEnforcer(viper.GetString("paths.perm_model"), adapter)

	if err != nil {
		logrus.Fatalf("failed to initialize new enforcer: %s", err.Error())
	}

	if err != nil {
		logrus.Fatalf("failed to initialize db: %s", err.Error())
	}

	// Инициализация OAuth2 сервисов
	config.InitOAuth2Config()
	config.InitVKAuthConfig()

	// Dependency Injection
	repos := repository.NewRepository(db, enforcer)
	service := service.NewService(repos)
	handlers := handler.NewHandler(service)

	srv := new(mainserver.Server)

	go func() {
		if err := srv.Run(viper.GetString("port"), handlers.InitRoutes()); err != nil {
			logrus.Fatalf("error occured while running http server: %s", err.Error())
		}
	}()

	logrus.Print("Main Server Started")

	// Реализация Graceful Shutdown
	// Блокировка функции main с помощью канала os.Signal
	quit := make(chan os.Signal, 1)

	// Запись в канал, если процесс, в котором выполняется приложение
	// получит сигнал SIGTERM или SIGINT
	signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT)

	// Чтение из канала, блокирующая выполнение функции main
	<-quit

	logrus.Print("Rental Housing Main Server Shutting Down")

	if err := srv.Shutdown(context.Background()); err != nil {
		logrus.Errorf("error occured on server shutting down: %s", err.Error())
	}

	if err := db.Close(); err != nil {
		logrus.Errorf("error occured on db connection close: %s", err.Error())
	}
}

/* Инициализация файлов конфигурации */
func initConfig() error {
	viper.AddConfigPath("config")
	viper.SetConfigName("config")

	return viper.ReadInConfig()
}

В данной точке входа основной интерес вызывает множество комментариев, которые задают базовые настройки для вывода Swagger-документации (аналогично замыканию options из сервиса NEJ).

// @title Основной сервис
// @version 1.0
// description Основной сервис

// @host localhost:5000
// @BasePath /

// @securityDefinitions.apikey ApiKeyAuth
// @in header
// @name Authorization

В данных комментариях содержится та же информация, что и содержалось в options для сервиса NEJ.

Пакет, который используется для автоматического документирования API маршрутов, называется swag. Данный пакет позволяет быстро и эффективно определить любой API.

Прежде чем приступить к обозреванию того, каким образом необходимо документировать API, подробнее разберём файловую структуру проекта, чтобы убедиться соответствию её архитектурному паттерну CSR.

Рисунок 8 - Содержимое директории pkg

Рисунок 8 – Содержимое директории pkg

В директории присутствует каталог handler (обработчики, они же – контроллеры), repository и service. Минимум необходимого для CSR наблюдается.

Если связь контроллеров с определённым адресом формируется в handler, то там и следует описывать API.

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

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

// @Summary Авторизация пользователя
// @Tags API для авторизации и регистрации пользователя
// @Description Авторизация пользователя
// @ID auth-sign-in
// @Accept  json
// @Produce  json
// @Param input body userModel.UserSignInModel true "credentials"
// @Success 200 {object} userModel.TokenAccessModel "data"
// @Failure 400,404 {object} httpModel.ResponseMessage
// @Failure 500 {object} httpModel.ResponseMessage
// @Failure default {object} httpModel.ResponseMessage
// @Router /auth/sign-in [post]
func (h *AuthHandler) signIn(c *gin.Context) {
    // Определение входной модели
	var input userModel.UserSignInModel

    // Связывание переменной входной модели с данными из пользовательского запроса
	if err := c.BindJSON(&input); err != nil {
		utilContext.NewErrorResponse(c, http.StatusBadRequest, "invalid input body")
		return
	}

    // Передача данных в слой сервисов
	data, err := h.services.Authorization.LoginUser(input)
	if err != nil {
		utilContext.NewErrorResponse(c, http.StatusBadRequest, err.Error())
		return
	}

	// Добавление токена обновления в http only cookie
	c.SetCookie(viper.GetString("environment.refresh_token_key"), data.RefreshToken,
		30*24*60*60*1000, "/", viper.GetString("environment.domain"), false, true)
	c.SetSameSite(config.HTTPSameSite)

    // Отправка ответа клиенту
	c.JSON(http.StatusOK, userModel.TokenAccessModel{
		AccessToken: data.AccessToken,
	})
}

Наибольший интерес представляют комментарии:

// @Summary Авторизация пользователя
// @Tags API для авторизации и регистрации пользователя
// @Description Авторизация пользователя
// @ID auth-sign-in
// @Accept  json
// @Produce  json
// @Param input body userModel.UserSignInModel true "credentials"
// @Success 200 {object} userModel.TokenAccessModel "data"
// @Failure 400,404 {object} httpModel.ResponseMessage
// @Failure 500 {object} httpModel.ResponseMessage
// @Failure default {object} httpModel.ResponseMessage
// @Router /auth/sign-in [post]

Здесь структура отличается от той, которая была использована в сервисе NEJ. В данном случае используется однострочный комментарий.

Поясним некоторые параметры при описании API:

  1. @ Summary – название запроса в документации

  2. @ Tags – теги для группировки API

  3. @ Description – описание запроса в документации

  4. @ ID – идентификатор API

  5. @ Accept – данные, которые будут приниматься на входе запроса

  6. @ Produce – формат данных, которые будут возвращаться в ответе

  7. @ Param input body userModel.UserSignInModel – параметры запроса

  8. @ Success 200 {object} userModel.TokenAccessModel “data” – код ответа, формат данных и описание

  9. @ Failure 400,404 {object} httpModel.ResponseMessage – код ответа, формат данных и описание

  10. @ Router /auth/sign-in [post] – путь запроса и метод запроса(HTTP)

Полное определение обработчиков для системы авторизации

package auth

import (
	"fmt"
	config "main-server/config"
	middlewareConstant "main-server/pkg/constant/middleware"
	pathConstant "main-server/pkg/constant/path"
	utilContext "main-server/pkg/handler/util"
	httpModel "main-server/pkg/model/http"
	userModel "main-server/pkg/model/user"
	"net/http"

	"github.com/gin-gonic/gin"
	uuid "github.com/satori/go.uuid"
	"github.com/spf13/viper"
)

// @Summary Регистрация нового пользователя
// @Tags API для авторизации и регистрации пользователя
// @Description Регистрация нового пользователя
// @ID auth-sign-up
// @Accept  json
// @Produce  json
// @Param input body userModel.UserSignUpModel true "account info"
// @Success 200 {object} userModel.TokenAccessModel "data"
// @Failure 400,404 {object} httpModel.ResponseMessage
// @Failure 500 {object} httpModel.ResponseMessage
// @Failure default {object} httpModel.ResponseMessage
// @Router /auth/sign-up [post]
func (h *AuthHandler) signUp(c *gin.Context) {
	var input userModel.UserSignUpModel
	if err := c.BindJSON(&input); err != nil {
		utilContext.NewErrorResponse(c, http.StatusBadRequest, err.Error())
		return
	}

	data, err := h.services.Authorization.CreateUser(input)
	if err != nil {
		utilContext.NewErrorResponse(c, http.StatusInternalServerError, err.Error())
		return
	}

	// Добавление токена обновления в http only cookie
	c.SetCookie(viper.GetString("environment.refresh_token_key"), data.RefreshToken,
		30*24*60*60*1000, "/", viper.GetString("environment.domain"), false, true)
	c.SetSameSite(config.HTTPSameSite)

	c.JSON(http.StatusOK, userModel.TokenAccessModel{
		AccessToken: data.AccessToken,
	})
}

// @Summary Загрузка пользовательского изображения
// @Tags API для авторизации и регистрации пользователя
// @Description Загрузка пользовательского изображения
// @ID auth-sign-up-upload-image
// @Accept  json
// @Produce  json
// @Param input body userModel.UserSignUpModel true "account info"
// @Success 200 {object} userModel.TokenAccessModel "data"
// @Failure 400,404 {object} httpModel.ResponseMessage
// @Failure 500 {object} httpModel.ResponseMessage
// @Failure default {object} httpModel.ResponseMessage
// @Router /auth/sign-up/upload/image [post]
func (h *AuthHandler) uploadProfileImage(c *gin.Context) {
	form, err := c.MultipartForm()
	if err != nil {
		utilContext.NewErrorResponse(c, http.StatusBadRequest, err.Error())
		return
	}

	// Получение информации о файле из формы
	images := form.File["file"]
	profileImage := images[len(images)-1]
	filepath := pathConstant.PUBLIC_USER + uuid.NewV4().String()

	_, err = h.services.Authorization.UploadProfileImage(c, filepath)
	if err != nil {
		utilContext.NewErrorResponse(c, http.StatusInternalServerError, err.Error())
		return
	}

	// Загрузка файла на сервер
	c.SaveUploadedFile(profileImage, filepath)

	c.JSON(http.StatusOK, httpModel.ResponseStatus{
		Status: "Изображение профиля пользователя было обновлено",
	})
}

// @Summary Авторизация пользователя
// @Tags API для авторизации и регистрации пользователя
// @Description Авторизация пользователя
// @ID auth-sign-in
// @Accept  json
// @Produce  json
// @Param input body userModel.UserSignInModel true "credentials"
// @Success 200 {object} userModel.TokenAccessModel "data"
// @Failure 400,404 {object} httpModel.ResponseMessage
// @Failure 500 {object} httpModel.ResponseMessage
// @Failure default {object} httpModel.ResponseMessage
// @Router /auth/sign-in [post]
func (h *AuthHandler) signIn(c *gin.Context) {
	var input userModel.UserSignInModel
	if err := c.BindJSON(&input); err != nil {
		utilContext.NewErrorResponse(c, http.StatusBadRequest, "invalid input body")
		return
	}

	data, err := h.services.Authorization.LoginUser(input)
	if err != nil {
		utilContext.NewErrorResponse(c, http.StatusBadRequest, err.Error())
		return
	}

	// Добавление токена обновления в http only cookie
	c.SetCookie(viper.GetString("environment.refresh_token_key"), data.RefreshToken,
		30*24*60*60*1000, "/", viper.GetString("environment.domain"), false, true)
	c.SetSameSite(config.HTTPSameSite)

	c.JSON(http.StatusOK, userModel.TokenAccessModel{
		AccessToken: data.AccessToken,
	})
}

// @Summary Авторизация пользователя через VK (no work)
// @Tags API для авторизации и регистрации пользователя
// @Description Авторизация пользователя через VK (no work)
// @ID auth-sign-in-vk
// @Accept  json
// @Produce  json
// @Param input body userModel.UserSignInModel true "credentials"
// @Success 200 {object} userModel.TokenAccessModel "data"
// @Failure 400,404 {object} httpModel.ResponseMessage
// @Failure 500 {object} httpModel.ResponseMessage
// @Failure default {object} httpModel.ResponseMessage
// @Router /auth/sign-in/vk [post]
func (h *AuthHandler) signInVK(c *gin.Context) {
	var input userModel.UserSignInModel

	if err := c.BindJSON(&input); err != nil {
		utilContext.NewErrorResponse(c, http.StatusBadRequest, "invalid input body")
		return
	}

	data, err := h.services.Authorization.LoginUser(input)
	if err != nil {
		utilContext.NewErrorResponse(c, http.StatusBadRequest, err.Error())
		return
	}

	// Добавление токена обновления в http only cookie
	c.SetCookie(viper.GetString("environment.refresh_token_key"), data.RefreshToken,
		30*24*60*60*1000, "/", viper.GetString("environment.domain"), false, true)
	c.SetSameSite(config.HTTPSameSite)

	c.JSON(http.StatusOK, userModel.TokenAccessModel{
		AccessToken: data.AccessToken,
	})
}

// @Summary Авторизация пользователя через Google OAuth2
// @Tags API для авторизации и регистрации пользователя
// @Description Авторизация пользователя через Google OAuth2
// @ID auth-sign-in-oauth2
// @Accept  json
// @Produce  json
// @Param input body userModel.GoogleOAuth2Code true "credentials"
// @Success 200 {object} userModel.TokenAccessModel "data"
// @Failure 400,404 {object} httpModel.ResponseMessage
// @Failure 500 {object} httpModel.ResponseMessage
// @Failure default {object} httpModel.ResponseMessage
// @Router /auth/sign-in/oauth2 [post]
func (h *AuthHandler) signInOAuth2(c *gin.Context) {
	var input userModel.GoogleOAuth2Code

	if err := c.BindJSON(&input); err != nil {
		utilContext.NewErrorResponse(c, http.StatusBadRequest, "invalid input body")
		return
	}

	// For fast tests
	/*token, _ := configs.AppOAuth2Config.GoogleLogin.Exchange(c, input.Code)
	_, _ = google_oauth2.RevokeToken(token.AccessToken)
	return*/

	data, err := h.services.Authorization.LoginUserOAuth2(input.Code)
	if err != nil {
		utilContext.NewErrorResponse(c, http.StatusBadRequest, err.Error())
		return
	}

	// Добавление токена обновления в http only cookie
	c.SetCookie(viper.GetString("environment.refresh_token_key"), data.RefreshToken,
		30*24*60*60*1000, "/", viper.GetString("environment.domain"), false, true)
	c.SetSameSite(config.HTTPSameSite)

	c.JSON(http.StatusOK, userModel.TokenAccessModel{
		AccessToken: data.AccessToken,
	})
}

// @Summary Обновление токена доступа
// @Tags API для авторизации и регистрации пользователя
// @Description Обновление токена доступа
// @ID auth-refresh
// @Accept  json
// @Produce  json
// @Param Authorization header string true "Токен доступа для текущего пользователя" example(Bearer access_token)
// @Success 200 {object} userModel.TokenAccessModel "data"
// @Failure 400,404 {object} httpModel.ResponseMessage
// @Failure 500 {object} httpModel.ResponseMessage
// @Failure default {object} httpModel.ResponseMessage
// @Router /auth/refresh [post]
func (h *AuthHandler) refresh(c *gin.Context) {

	// Получение токена обновления из файла cookie
	refreshToken, err := c.Cookie(viper.GetString("environment.refresh_token_key"))

	fmt.Println(refreshToken)

	if err != nil {
		utilContext.NewErrorResponse(c, http.StatusUnauthorized, err.Error())
		return
	}

	// Получение дополнительной авторизационной информации пользователя (после работы middleware цепочки)
	accessToken, _ := c.Get(middlewareConstant.ACCESS_TOKEN_CTX)
	authTypeValue, _ := c.Get(middlewareConstant.AUTH_TYPE_VALUE_CTX)
	tokenApi, _ := c.Get(middlewareConstant.TOKEN_API_CTX)

	// Обновление токена доступа
	data, err := h.services.Authorization.Refresh(userModel.TokenLogoutDataModel{
		AccessToken:   accessToken.(string),
		RefreshToken:  refreshToken,
		AuthTypeValue: authTypeValue.(string),
		TokenApi:      tokenApi.(*string),
	}, refreshToken)

	if err != nil {
		utilContext.NewErrorResponse(c, http.StatusUnauthorized, err.Error())
		return
	}

	// Установка нового токена обновления (необходимо, если токен обновления изменился)
	c.SetCookie(viper.GetString("environment.refresh_token_key"), data.RefreshToken,
		30*24*60*60*1000, "/", viper.GetString("environment.domain"), false, true)
	c.SetSameSite(config.HTTPSameSite)

	c.JSON(http.StatusOK, userModel.TokenAccessModel{
		AccessToken: data.AccessToken,
	})
}

type LogoutOutputModel struct {
	IsLogout bool `json:"is_logout"`
}

// @Summary Выход из аккаунта
// @Tags API для авторизации и регистрации пользователя
// @Description Выход из аккаунта
// @ID auth-logout
// @Accept  json
// @Produce  json
// @Param Authorization header string true "Токен доступа для текущего пользователя" example(Bearer access_token)
// @Success 200 {object} LogoutOutputModel "data"
// @Failure 400,404 {object} httpModel.ResponseMessage
// @Failure 500 {object} httpModel.ResponseMessage
// @Failure default {object} httpModel.ResponseMessage
// @Router /auth/logout [post]
func (h *AuthHandler) logout(c *gin.Context) {
	refreshToken, err := c.Cookie(viper.GetString("environment.refresh_token_key"))

	if err != nil {
		utilContext.NewErrorResponse(c, http.StatusBadRequest, err.Error())
		return
	}

	accessToken, _ := c.Get(middlewareConstant.ACCESS_TOKEN_CTX)
	authTypeValue, _ := c.Get(middlewareConstant.AUTH_TYPE_VALUE_CTX)
	tokenApi, _ := c.Get(middlewareConstant.TOKEN_API_CTX)

	data, err := h.services.Authorization.Logout(userModel.TokenLogoutDataModel{
		AccessToken:   accessToken.(string),
		RefreshToken:  refreshToken,
		AuthTypeValue: authTypeValue.(string),
		TokenApi:      tokenApi.(*string),
	})

	if err != nil {
		utilContext.NewErrorResponse(c, http.StatusInternalServerError, err.Error())
		return
	}

	if data {
		c.SetCookie(viper.GetString("environment.refresh_token_key"), "",
			30*24*60*60*1000, "/", viper.GetString("environment.domain"), false, true)
		c.SetSameSite(config.HTTPSameSite)
	}

	c.JSON(http.StatusOK, LogoutOutputModel{
		IsLogout: data,
	})
}

// @Summary Активация аккаунта по почте
// @Tags API для авторизации и регистрации пользователя
// @Description Активация аккаунта по почте
// @ID auth-activate
// @Accept  json
// @Produce  json
// @Success 200 {object} LogoutOutputModel "data"
// @Failure 400,404 {object} httpModel.ResponseMessage
// @Failure 500 {object} httpModel.ResponseMessage
// @Failure default {object} httpModel.ResponseMessage
// @Router /auth/activate [get]
func (h *AuthHandler) activate(c *gin.Context) {
	_, err := h.services.Activate(c.Params.ByName("link"))

	if err != nil {
		utilContext.NewErrorResponse(c, http.StatusBadRequest, err.Error())
		return
	}

	c.HTML(http.StatusOK, "account_activate.html", gin.H{
		"title": "Подтверждение аккаунта",
	})
}

// @Summary Запрос на смену пароля пользователем
// @Tags API для авторизации и регистрации пользователя
// @Description Запрос на смену пароля пользователем
// @ID auth-recovery-password
// @Accept  json
// @Produce  json
// @Param input body userModel.UserEmailModel true "credentials"
// @Success 200 {object} httpModel.ResponseMessage "data"
// @Failure 400,404 {object} httpModel.ResponseMessage
// @Failure 500 {object} httpModel.ResponseMessage
// @Failure default {object} httpModel.ResponseMessage
// @Router /auth/recovery/password [post]
func (h *AuthHandler) recoveryPassword(c *gin.Context) {
	var input userModel.UserEmailModel

	if err := c.BindJSON(&input); err != nil {
		utilContext.NewErrorResponse(c, http.StatusBadRequest, "invalid input body")
		return
	}

	_, err := h.services.Authorization.RecoveryPassword(input.Email)
	if err != nil {
		utilContext.NewErrorResponse(c, http.StatusInternalServerError, err.Error())
		return
	}

	c.JSON(http.StatusOK, httpModel.ResponseMessage{
		Message: "На Вашу почту была отправлена ссылка с подтверждением изменения пароля",
	})
}

// @Summary Изменение пароля пользователем
// @Tags API для авторизации и регистрации пользователя
// @Description Изменение пароля пользователем
// @ID auth-reset-password
// @Accept  json
// @Produce  json
// @Param input body userModel.ResetPasswordModel true "credentials"
// @Success 200 {object} httpModel.ResponseMessage "data"
// @Failure 400,404 {object} httpModel.ResponseMessage
// @Failure 500 {object} httpModel.ResponseMessage
// @Failure default {object} httpModel.ResponseMessage
// @Router /auth/reset/password [post]
func (h *AuthHandler) resetPassword(c *gin.Context) {
	var input userModel.ResetPasswordModel

	if err := c.BindJSON(&input); err != nil {
		utilContext.NewErrorResponse(c, http.StatusBadRequest, "invalid input body")
		return
	}

	_, err := h.services.Authorization.ResetPassword(input)
	if err != nil {
		utilContext.NewErrorResponse(c, http.StatusInternalServerError, err.Error())
		return
	}

	c.JSON(http.StatusOK, httpModel.ResponseMessage{
		Message: "Пароль был успешно изменён!",
	})
}

Для генерации документации достаточно выполнить команду swag init -g cmd/main.go

Также важно, чтобы перед генерацией документации был определён маршрут, по которому будет осуществлён вывод документации, сгенерированной локально:

package handler

import (
	middlewareConstant "main-server/pkg/constant/middleware"
	authHandler "main-server/pkg/handler/auth"
	serviceHandler "main-server/pkg/handler/service"

	"github.com/gin-contrib/cors"
	"github.com/gin-gonic/gin"
	"github.com/spf13/viper"

	_ "main-server/docs"

	service "main-server/pkg/service"

	_ "github.com/swaggo/files"
	swaggerFiles "github.com/swaggo/files"
	_ "github.com/swaggo/gin-swagger"
	ginSwagger "github.com/swaggo/gin-swagger"
)

type Handler struct {
	services *service.Service
}

func NewHandler(services *service.Service) *Handler {
	return &Handler{services: services}
}

/* Инициализация маршрутов */
func (h *Handler) InitRoutes() *gin.Engine {
	router := gin.New()

	// Установка максимального размера тела Multipart
	router.MaxMultipartMemory = 50 << 20 // 50 MiB

	// Установка статической директории
	router.Static("/public", "./public")

	// Установка глобального каталога для хранения HTML-страниц
	router.LoadHTMLGlob("pkg/template/*")

	// Установка CORS-политик
	router.Use(cors.New(cors.Config{
		//AllowAllOrigins: true,
		AllowOrigins:     []string{viper.GetString("client_url")},
		AllowMethods:     []string{"POST", "GET"},
		AllowHeaders:     []string{"Origin", "Content-type", "Authorization"},
		AllowCredentials: true,
	}))

	// URL: /swagger/index.html
	router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))

	// Инициализация списка обработчиков в цепочке middleware
	middleware := make(map[string]func(c *gin.Context))
	middleware[middlewareConstant.MN_UI] = h.userIdentity
	middleware[middlewareConstant.MN_UI_LOGOUT] = h.userIdentityLogout

	// Инициализация маршрутов для сервиса service
	service := serviceHandler.NewServiceHandler(router, h.services)
	service.InitRoutes(&middleware)

	// Инициализация маршрутов для сервиса auth
	auth := authHandler.NewAuthHandler(router, h.services)
	auth.InitRoutes(&middleware)

	return router
}

Код добавления просмотра документации по определённому пути выглядит следующим образом:

// URL: /swagger/index.html
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))

При запуске сервиса мы получим возможность просмотра документации онлайн.

Рисунок 9 - Просмотр документации сервиса GG онлайн (аналогично сервису NEJ)

Рисунок 9 – Просмотр документации сервиса GG онлайн (аналогично сервису NEJ)

Вывод

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

Был предложен ряд пакетов, которые можно использовать для решения этой задачи как на Express.js (swagger-express-generate, swagger-ui-express), так и на Gin (swag). Читатель может обратиться к исходному коду и основываясь на данных примерах реализовать документирование API на своих сервисах.

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

Ссылки на использованные источники

  1. Сервис NEJ

  2. Сервис GG

  3. Controller Service Repository

  4. Шаблон для работы с swagger-express-generate

Создание документации вручную – утомительный процесс. В этой статье мы рассмотрим основы Swagger и его возможности по документированию REST API в SpringBoot приложении.

Спонсор поста

Используемые версии

Java 17
Spring Boot 3.0.2
Swagger 2.2.8
SpringDoc 1.6.14 | 2.0.2

Изменения статьи

29.06.22: Обновил до Java 17. Обновил все зависимости до актуальных.
11.02.23: Обновил SpringBoot до 3.0.2. Обновил остальные зависимости. Добавил раздел с авторизацией в Swagger UI.

Swagger позволяет разработчикам описывать структуру своих API и генерировать интерактивную документацию, клиентские библиотеки и серверные модули для реализации API на разных языках.

Swagger предоставляет спецификацию для документирования REST API, которая называется OpenAPI Specification (OAS). Эта спецификация предоставляет четкий и лаконичный способ описания эндпойнтов, их параметров, моделей запросов и ответов и других аспектов API.

В данной статье рассматривается пример генерации OAS на основе уже существующих REST контроллеров. Мы разметим контроллеры и эндпойнты аннотациями, на основе которых будет сгенерирована OAS.

Существуют библиотеки, которые на основе OAS могут сгенерировать интерактивную документацию для API, которая позволит отправлять запросы, и получать ответы. Мы воспользуемся библиотекой SpringDoc.

Почему SpringDoc, а не SpringFox?

В интернете множество статей с использованием библиотеки SpringFox для генерации Swagger UI. Почему тогда я использую какой-то SpringDoc?

SpringFox был заброшен авторами. Последний коммит сделан в 2020 году, а количество issue уже перевалило за 200. В какой-то момент я столкнулся с каким-то багом в SpringFox, который никогда не будет исправлен, и стал искать альтернативы.

Такой альтернативой оказался SpringDoc, который активно развивается и поддерживает SpringBoot 3. Я успешно и быстро перевел свой проект с SpringFox на SpringDoc.

Также стоит упомянуть, что Swagger позволяет сгенерировать непосредственно код клиента или сервера по имеющейся OAS, для этого нужен генератор кода Swagger-Codegen.

Генератором я никогда не пользовался. Генерируемый код получается весьма неказистым и проще написать его самостоятельно.

Демо проект с REST API

Чтобы документировать API, для начала напишем его. Вы можете перейти к следующей главе, чтобы не тратить время.

Добавим примитивные контроллеры и одно DTO. Бизнес суть нашей системы – программа лояльности пользователей.

Для наших примеров достаточно слоя контроллеров, поэтому я позволю себе вольность опустить серверный и репозиторный слой и добавить бизнес логику непосредственно контроллер.

В качестве DTO у нас будет класс UserDto – это пользователь нашей системы. У него пять полей, из которых 3 обязательны.

UserDto.java

public class UserDto {

    private String key;
    private String name;
    private Long points = 0L;
    private Gender gender;
    private LocalDateTime regDate = LocalDateTime.now();

    public UserDto() {
    }

    public UserDto(String key, String name, Gender gender) {
        this.key = key;
        this.name = name;
        this.gender = gender;
    }

    public static UserDto of(String key, String value, Gender gender) {
        return new UserDto(key, value, gender);
    }

    // getters and setters

}
public enum Gender {
    MAN, WOMAN
}

Для взаимодействия с нашей бизнес-логикой, добавим три контроллера: UserController, PointContoller, SecretContoller.

UserController отвечает за добавление, обновление и получение пользователей.

UserController.java

@RestController
@RequestMapping("/api/user")
public class UserController {

    private final Map<String, UserDto> repository;

    public UserController(Map<String, UserDto> repository) {
        this.repository = repository;
    }

    @PutMapping(produces = APPLICATION_JSON_VALUE)
    public HttpStatus registerUser(@RequestBody UserDto userDto) {
        repository.put(userDto.getKey(), userDto);
        return HttpStatus.OK;
    }

    @PostMapping(produces = APPLICATION_JSON_VALUE)
    public HttpStatus updateUser(@RequestBody UserDto userDto) {
        if (!repository.containsKey(userDto.getKey())) return HttpStatus.NOT_FOUND;
        repository.put(userDto.getKey(), userDto);
        return HttpStatus.OK;
    }

    @GetMapping(value = "{key}", produces = APPLICATION_JSON_VALUE)
    public ResponseEntity<UserDto> getSimpleDto(@PathVariable("key") String key) {
        return ResponseEntity.ok(repository.get(key));
    }

}

PointContoller отвечает за взаимодействие с баллами пользователя. Один метод этого контроллера отвечает за добавление и удаление балов пользователям.

PointContoller.java

@RestController
@RequestMapping("api/user/point")
public class PointController {

    private final Map<String, UserDto> repository;

    public PointController(Map<String, UserDto> repository) {
        this.repository = repository;
    }

    @PostMapping("{key}")
    public HttpStatus changePoints(
            @PathVariable String key,
            @RequestParam("point") Long point,
            @RequestParam("type") String type
    ) {
        final UserDto userDto = repository.get(key);
        userDto.setPoints(
                "plus".equalsIgnoreCase(type)
                    ? userDto.getPoints() + point
                    : userDto.getPoints() - point
        );
        return HttpStatus.OK;
    }

}

Метод destroy в SecretContoller может удалить всех пользователей.

SecretContoller.java

@RestController
@RequestMapping("api/secret")
public class SecretController {

    private final Map<String, UserDto> repository;

    public SecretController(Map<String, UserDto> repository) {
        this.repository = repository;
    }

    @GetMapping(value = "destroy")
    public HttpStatus destroy() {
        repository.clear();
        return HttpStatus.OK;
    }

}

Настраиваем Swagger

😺

Ветка в репозитории: spring-boot-3

Теперь добавим Swagger в наш проект. Для этого добавьте следующие зависимости в pom.xml:

<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.0.2</version>
</dependency>
Актуальная версия в Maven Central: SpringDoc Starter

Для WebFlux используйте другую зависимость:

<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
    <version>2.0.2</version>
</dependency>
Актуальная версия в Maven Central: SpringDoc Starter WebFlux

Данные зависимости подходят только для проектов на SpringBoot 3. Если вы используете SpringBoot 2, то вам необходимо добавить другие зависимости:

<dependency>
    <groupId>io.swagger.core.v3</groupId>
    <artifactId>swagger-annotations</artifactId>
    <version>2.2.8</version>
</dependency>
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-ui</artifactId>
    <version>1.6.14</version>
</dependency>
Актуальные вресии в Maven Central: Swagger, SpringDoc

Swagger автоматически находит список всех контроллеров. При нажатии на любой из них будут перечислены допустимые методы HTTP (DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT).

Для каждого метода доступные следующие данные: статус ответа, тип содержимого и список параметров.

Поэтому после добавления зависимостей у нас уже есть документация, доступная по ссылке: http://localhost:8080/swagger-ui. А также есть OAS, доступный по адресу: http://localhost:8080/v3/api-docs.

Swagger запущенный с дефолтными настройками

Swagger запущенный с дефолтными настройками

Также можно вызвать каждый метод с помощью пользовательского интерфейса. Откроем метод добавления пользователей.

Пока у нас не очень информативная документация. Давайте исправим это. Для начала создадим класс конфигурации сваггера OpenApiConfig – имя произвольное.

@OpenAPIDefinition(
        info = @Info(
                title = "Loyalty System Api",
                description = "Loyalty System", version = "1.0.0",
                contact = @Contact(
                        name = "Struchkov Mark",
                        email = "mark@struchkov.dev",
                        url = "https://mark.struchkov.dev"
                )
        )
)
public class OpenApiConfig {
    
}
  • title – это название вашего приложения.
  • version – версия вашего API.
  • contact – ответственные за API.

Эти данные больше для визуальной красоты UI документации.

Разметка контроллеров

Переопределим описания контроллеров, чтобы сделать документацию понятнее. Для этого пометим контроллеры аннотацией @Tag.

@Tag(name="Название контроллера", description="Описание контролера")
public class ControllerName {

    // ... ... ... ... ...

} 

Добавили описание контроллеров в Swagger

Скрыть контроллер

У нас есть контроллер, который мы хотим скрыть – SecretController. Аннотация @Hidden поможет нам в этом.

@Hidden
@Tag(name = "Секретный контролер", description = "Позволяет удалить всех пользователей")
public class SecretController {

    // ... ... ... ... ...

}

Аннотация скрывает контроллер только из Swagger. Он все также доступен для вызова. Используйте другие методы для защиты вашего API. Например, авторизацию на основе JWT токена.

Наша документация стала намного понятнее, но давайте добавим описания для каждого метода контроллера.

Разметка методов

Аннотация @Operation описывает возможности методов контроллера. Достаточно определить следующие значения:

  • summary – короткое описание.
  • description – более полное описание.
@Operation(
	summary = "Регистрация пользователя",
	description = "Позволяет зарегистрировать пользователя"
)
public HttpStatus registerUser(@RequestBody UserDto userDto) {

    // ... ... ... ... ...

}

Разметка переменных метода

При помощи аннотации Parameter также опишем переменные в методе, который отвечает за управление баллами пользователей.

public HttpStatus changePoints(
    @PathVariable @Parameter(description = "Идентификатор пользователя") String key,
    @RequestParam("point") @Parameter(description = "Количество баллов") Long point,
    @RequestParam("type") @Parameter(description = "Тип операции") TypeOperation type
) {

    // ... ... ... ... ...

}

С помощью параметра required можно задать обязательные поля для запроса. По умолчанию все поля необязательные.

Разметка DTO

Разработчики стараются называть переменные в классе понятными именами, но не всегда это помогает. Вы можете дать человеко-понятное описание самой DTO и ее переменным с помощью аннотации @Schema.

@Schema(description = "Сущность пользователя")
public class UserDto {

    @Schema(description = "Идентификатор")
    private String key;

    // ... ... ... ... ...

}

Сваггер заполнит переменные, формат которых он понимает: enum, даты. Но если некоторые поля DTO имеют специфичный формат, то помогите разработчикам добавив пример.

@Schema(description = "Идентификатор", example = "A-124523")

Выглядеть это будет так:

В разделе Schema и при отправке запроса

Но подождите, зачем мы передаем дату регистрации. Да и уникальный ключ чаще всего будет задаваться сервером. Скроем эти поля из swagger с помощью параметра Schema.AccessMode.READ_ONLY:

public class UserDto {

    @Schema(accessMode = Schema.AccessMode.READ_ONLY)
    private String key;

    // ... ... ... ... ...

}

Валидация

Про валидацию я подробно рассказывал в статье: “Валидация данных в SpringBoot”. Здесь я лишь хочу показать, что валидация параметров методов контроллеров также отображается в Swagger.

Добавим валидацию в метод управления баллами пользователя в PointController. Мы не хотим, чтобы можно было передать отрицательные баллы.

public HttpStatus changePoints(
    // ... ... ... ... ...
    @RequestParam("point") @Min(0) @Parameter(description = "Количество баллов") Long point,
    // ... ... ... ... ...
) {

    // ... ... ... ... ...

}

Давайте посмотрим на изменения спецификации.

Для поля point появилось замечание minimum: 0. И все это нам не стоило ни малейшего дополнительного усилия.

Авторизация в Swager

😺

Ветка в репозитории: jwt-auth

Если ваш API защищен авторизаций и аутентификаций, то вы не сможете просто так вызвать запрос из Swagger. Один из самых распространенных способов авторизации это JWT, о нем я рассказывал в отдельной статье.

Сейчас нам нужно объяснить сваггеру, какая авторизация у нас применяется и какие эндпойнты ей защищены.

Авторизация с использованием JWT

В первом случае рассмотрим старый добрый JWT. Swagger должен получить access-токен и добавлять его в Header запросов.

Начнем с добавления аннотации @SecurityScheme над классом OpenApiConfig:

@OpenAPIDefinition(...)
@SecurityScheme(
        name = "JWT",
        type = SecuritySchemeType.HTTP,
        bearerFormat = "JWT",
        scheme = "bearer"
)
public class OpenApiConfig {

}

Мы описали схему авторизации с использованием JWT. Теперь пометим аннотацией @SecurityRequirement все эндпойнты, которые используют данный способ авторизации. В @SecurityRequirement в атрибуте name указываем название нашей схемы – JWT. Название должно совпадать с названием из аннотации @SecurityScheme.

@PostMapping("{key}")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Управление баллами", description = "Позволяет удалить или добавить баллы пользователю")
public HttpStatus changePoints(...) {
	... ... ...
}
Пример помеченного контроллера

Запускаем приложение и открываем Swagger. Видим, что появилась кнопка Authorize. Нажмем на нее и получим возможность установить JWT токен для всех методов. Защищенные методы, которые мы пометили, будут отмечены значком замка.

Устанавливаем токен, после чего нажимаем кнопку Authorize. Переходим к защищенному эндпойнту и вызываем его.

Видим, что Swagger проставил заголовок Authorization, а также сам добавил Bearer к токену. То что нам и было нужно.

Авторизация с использованием Ouath2

😺

Ветка в репозитории: swagger-oauth2

С Oauth2 все оказалось сложнее. Проблема в том, что при авторизации с использованием Oauth2 SpringBoot генерирует JSESSIONID куку, которую сохраняет в браузере. Дальше браузер передает эту куку с каждым запросом, что позволяет SpringBoot приложению понимать кто с ним общается.

Swagger же при Oauth2 авторизации генерирует себе access token, который пытается использовать при запросе. Проблема в том, что он никак не может сам получить значение JSESSIONID, так как его генерирует Spring после успешной Oauth2 авторизации.

Если вы знаете, как решить эту проблему, пожалуйста напишите в комментариях. Я рабочего решения быстро найти не смог.

Поэтому для Ouath2 воспользуемся возможностью сваггера передавать куку.

@OpenAPIDefinition(...)
@SecurityScheme(
        name = "jsessionid",
        in = SecuritySchemeIn.COOKIE,
        type = SecuritySchemeType.APIKEY,
        paramName = "JSESSIONID"
)
public class OpenApiConfig {

}

Помечаем эндпойнты аннотацией:

@SecurityRequirement(name = "jsessionid")

Далее переходим по какому-нибудь урлу нашего API, видим Oauth2 окно авторизации. Проходим авторизацию. Теперь открываем консоль разработчика в браузере и находим раздел с куками.

Вместо настоящего Oauth2 сервера можно использовать mock-server. Который принимает любой email и любой пароль. Удобно использовать при локальной разработке. Конфигурацию подключения можно посмотреть в моей заметке.

Нас интересует кука JSESSIONID, берем ее и вставляем в окно авторизации в Swagger.

Вот и все. Это будет работать.

Итог

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

OAS 3 This page is about OpenAPI 3.0. If you use OpenAPI 2.0, visit OpenAPI 2.0 pages.

You can write OpenAPI definitions in YAML or JSON. In this guide, we use only YAML examples but JSON works equally well. A sample OpenAPI 3.0 definition written in YAML looks like:

openapi: 3.0.0
info:
  title: Sample API
  description: Optional multiline or single-line description in [CommonMark](http://commonmark.org/help/) or HTML.
  version: 0.1.9

servers:
  - url: http://api.example.com/v1
    description: Optional server description, e.g. Main (production) server
  - url: http://staging-api.example.com
    description: Optional server description, e.g. Internal staging server for testing

paths:
  /users:
    get:
      summary: Returns a list of users.
      description: Optional extended description in CommonMark or HTML.
      responses:
        '200':    # status code
          description: A JSON array of user names
          content:
            application/json:
              schema: 
                type: array
                items: 
                  type: string

All keyword names are case-sensitive.

Metadata

Every API definition must include the version of the OpenAPI Specification that this definition is based on:

openapi: 3.0.0

The OpenAPI version defines the overall structure of an API definition – what you can document and how you document it. OpenAPI 3.0 uses semantic versioning with a three-part version number. The available versions are 3.0.0, 3.0.1, 3.0.2, and 3.0.3; they are functionally the same.

The info section contains API information: title, description (optional), version:

info:
  title: Sample API
  description: Optional multiline or single-line description in [CommonMark](http://commonmark.org/help/) or HTML.
  version: 0.1.9

title is your API name. description is extended information about your API. It can be multiline and supports the CommonMark dialect of Markdown for rich text representation. HTML is supported to the extent provided by CommonMark (see HTML Blocks in CommonMark 0.27 Specification). version is an arbitrary string that specifies the version of your API (do not confuse it with file revision or the openapi version). You can use semantic versioning like major.minor.patch, or an arbitrary string like 1.0-beta or 2017-07-25. info also supports other keywords for contact information, license, terms of service, and other details.

Reference: Info Object.

Servers

The servers section specifies the API server and base URL. You can define one or several servers, such as production and sandbox.

servers:
  - url: http://api.example.com/v1
    description: Optional server description, e.g. Main (production) server
  - url: http://staging-api.example.com
    description: Optional server description, e.g. Internal staging server for testing

All API paths are relative to the server URL. In the example above, /users means http://api.example.com/v1/users or http://staging-api.example.com/users, depending on the server used. For more information, see API Server and Base Path.

Paths

The paths section defines individual endpoints (paths) in your API, and the HTTP methods (operations) supported by these endpoints. For example, GET /users can be described as:

paths:
  /users:
    get:
      summary: Returns a list of users.
      description: Optional extended description in CommonMark or HTML
      responses:
        '200':
          description: A JSON array of user names
          content:
            application/json:
              schema: 
                type: array
                items: 
                  type: string

An operation definition includes parameters, request body (if any), possible response status codes (such as 200 OK or 404 Not Found) and response contents. For more information, see Paths and Operations.

Parameters

Operations can have parameters passed via URL path (/users/{userId}), query string (/users?role=admin), headers (X-CustomHeader: Value) or cookies (Cookie: debug=0). You can define the parameter data types, format, whether they are required or optional, and other details:

paths:
  /users/{userId}:
    get:
      summary: Returns a user by ID.
      parameters:
        - name: userId
          in: path
          required: true
          description: Parameter description in CommonMark or HTML.
          schema:
            type : integer
            format: int64
            minimum: 1
      responses: 
        '200':
          description: OK

For more information, see Describing Parameters.

Request Body

If an operation sends a request body, use the requestBody keyword to describe the body content and media type.

paths:
  /users:
    post:
      summary: Creates a user.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                username:
                  type: string
      responses: 
        '201':
          description: Created

For more information, see Describing Request Body.

Responses

For each operation, you can define possible status codes, such as 200 OK or 404 Not Found, and the response body schema. Schemas can be defined inline or referenced via $ref. You can also provide example responses for different content types:

paths:
  /users/{userId}:
    get:
      summary: Returns a user by ID.
      parameters:
        - name: userId
          in: path
          required: true
          description: The ID of the user to return.
          schema:
            type: integer
            format: int64
            minimum: 1
      responses:
        '200':
          description: A user object.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: integer
                    format: int64
                    example: 4
                  name:
                    type: string
                    example: Jessica Smith
        '400':
          description: The specified user ID is invalid (not a number).
        '404':
          description: A user with the specified ID was not found.
        default:
          description: Unexpected error

Note that the response HTTP status codes must be enclosed in quotes: “200” (OpenAPI 2.0 did not require this). For more information, see Describing Responses.

Input and Output Models

The global components/schemas section lets you define common data structures used in your API. They can be referenced via $ref whenever a schema is required – in parameters, request bodies, and response bodies. For example, this JSON object:

{
  "id": 4,
  "name": "Arthur Dent"
}

can be represented as:

components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: integer
          example: 4
        name:
          type: string
          example: Arthur Dent
      # Both properties are required
      required:  
        - id
        - name

and then referenced in the request body schema and response body schema as follows:

paths:
  /users/{userId}:
    get:
      summary: Returns a user by ID.
      parameters:
        - in: path
          name: userId
          required: true
          schema:
            type: integer
            format: int64
            minimum: 1
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'    # <-------
  /users:
    post:
      summary: Creates a new user.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/User'      # <-------
      responses:
        '201':
          description: Created

Authentication

The securitySchemes and security keywords are used to describe the authentication methods used in your API.

components:
  securitySchemes:
    BasicAuth:
      type: http
      scheme: basic

security:
  - BasicAuth: []

Supported authentication methods are:

  • HTTP authentication: Basic, Bearer, and so on.
  • API key as a header or query parameter or in cookies
  • OAuth 2
  • OpenID Connect Discovery

For more information, see Authentication.

Full Specification

The full OpenAPI 3.0 Specification is available on GitHub: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md

Did not find what you were looking for? Ask the community
Found a mistake? Let us know

Edit me

Swagger UI предоставляет Фреймворк, который читает спецификацию OpenAPI. и создает веб-страницу с интерактивной документацией. В этом руководстве показано, как интегрировать документ спецификации OpenAPI в интерфейс Swagger.

Концептуальный обзор OpenAPI и Swagger можно посмотреть в разделе Знакомство со спецификациями OpenAPI и Swagger. Пошаговое руководство по созданию документа спецификации OpenAPI смотрим в Обзоре руководства OpenAPI 3.0.

Swagger UI – один из самых популярных инструментов для создания интерактивной документации. Swagger UI создает интерактивную консоль API для экспериментов с запросами в реальном времени. Кроме того, Swagger UI (активно управляемый проект с лицензией Apache 2.0) поддерживает последнюю версию спецификации OpenAPI (3.x) и интегрируется с другими инструментами Swagger.

Прежде чем мы углубимся в Swagger, нужно прояснить ключевые термины.

Swagger

Относится к инструментам API, связанным со спецификацией OpenAPI. Некоторыми из этих инструментов являются Swagger Editor, Swagger UI, Swagger Codegen, SwaggerHub и другие. Всеми инструментами управляет компания Smartbear. Для получения дополнительной информации см. Инструменты Swagger. «Swagger» являлся изначально оригинальным названием спецификации OpenAPI, но позже имя было изменено на OpenAPI, чтобы усилить открытый, не лицензионный характер стандарта. Люди иногда ссылаются на оба имени взаимозаменяемо (особенно на старых веб-страницах), но «OpenAPI» – это то, как следует обращаться к спецификации. Дополнительные сведения о разнице между OpenAPI и Swagger см. В разделе «В чем разница между Swagger и OpenAPI?».

OpenAPI

Официальное название спецификации OpenAPI. Спецификация OpenAPI предоставляет набор свойств, которые можно использовать для описания REST API. Рабочий, валидный документ можно использовать для создания интерактивной документации, создания клиентских SDK, запуска модульных тестов и многого другого. Подробности спецификации можно изучить на GitHub по адресу https://github.com/OAI/OpenAPI-Specification. В рамках инициативы Open API с Linux Foundation спецификация OpenAPI направлена ​​на то, чтобы быть независимой от производителя (многие компании участвуют в ее разработке).

Swagger Editor

Онлайн-редактор, который проверяет документацию OpenAPI на соответствие правилам спецификации OpenAPI. Редактор Swagger помечает ошибки и дает советы по форматированию.

Swagger UI

Веб-фрэймворк (на GitHub), который анализирует документ в спецификации OpenAPI и создает веб-страницу интерактивной документации. Swagger UI – это инструмент, который превращает спецификацию в подобный Petstore-сайт.

Swagger Codegen

Генерирует код SDK для множества различных платформ (таких как Java, JavaScript, Scala, Python, PHP, Ruby, Scala и другие). Код SDK помогает разработчикам интегрировать API на конкретной платформе и обеспечивает более надежные реализации, которые могут включать в себя больше масштабирования, многопоточности и т.д.. В общем, SDK – это наборы инструментов для реализации запросов, сделанных с помощью API. Swagger Codegen генерирует клиентские SDK практически на каждом языке программирования. См. Swagger Codegen для получения дополнительной информации. Смотрите также SDK и примеры приложений.

Знакомство со Swagger при помощи Petstore

Чтобы лучше понять интерфейс Swagger, давайте рассмотрим пример Swagger Petstore. В примере Petstore сайт генерируется с помощью Swagger UI.

Petstore

Конечные точки сгруппированы следующим образом:

  • pet
  • store
  • user

Авторизация запроса

Прежде чем делать какие-либо запросы, нужна авторизация. Нажимаем кнопку Authorize и заполняем информацию, требуемую в окне «Авторизация», изображенном ниже:

Authorize

Пример Petstore имеет модель безопасности OAuth 2.0. Код авторизации только для демонстрационных целей. Нет никакой реальной логики авторизации этих запросов, поэтому просто закрываем окно Авторизации.

Создание запроса

Теперь создадим запрос:

  • Разворачиваем конечную точку POST Pet
  • Нажимаем кнопку Try it out

Try it out

После того, как мы нажмем кнопку Try it out, значение примера в поле «Тело запроса» станет редактируемым.

  • В поле «Example Value» изменяем первое значение id на случайное целое число, например 193844. Также значение второго name на другое (имя вашего питомца).
  • Нажимаем Execute.

Execute

Выполнение примера запроса Petstore

Пользовательский интерфейс Swagger отправляет запрос и показывает отправленный curl. Раздел Ответы показывает ответ. (Если выбрать JSON вместо XML в раскрывающемся списке «Response content type», формат ответа будет показан в формате JSON.)

Response

Проверка создания питомца

  1. Разворачиваем точку GET /pet/{petId}
  2. Нажимаем кнопку Try it out
  3. Вводим ID питомца, который использовали в предыдущей операции. (Если забыли ID, посмотрите на конечную точку POST Pet, чтобы проверить значение.)
  4. Нажимаем Execute . В ответе мы должны увидеть имя нашего питомца.

Примеры сайтов с документаций по Swagger UI

Прежде чем мы перейдем к другому API с этим пособием по Swagger (кроме демонстрации Petstore), посмотрим на другие реализации Swagger:

  • Reverb
  • VocaDB
  • Watson Developer Cloud
  • The Movie Database API
  • Zomato API

Некоторые из этих сайтов выглядят одинаково, но другие, такие как The Movie Database API и Zomato, были легко интегрированы в остальную часть их сайта документации.

Глядя на примеры, можно заметить краткость документации в реализации Swagger. Эта краткость объясняется тем, что дисплей Swagger предназначен для интерактивного взаимодействия, где можно опробовать вызовы и посмотреть ответы – используя свой собственный ключ API, чтобы увидеть свои собственные данные. такой подход получил название: «учись, практикуясь». Кроме того, Swagger UI охватывает только документацию конечных точек. Концептуальные разделы обычно рассматриваются в отдельном руководстве.

👨‍💻 Практическое занятие: Создание спецификации OpenAPI в Swagger UI

На этом занятии мы создадим документацию в Swagger UI в спецификации OpenAPI. Если вы используете один из предварительно созданных файлов OpenAPI, вы можете увидеть демонстрацию того, что мы создадим здесь: OpenWeatherMap Swagger UI или Sunrise/sunset Swagger UI).

OpenWeatherMap

Для интеграции спецификации OpenAPI в Swagger UI:

  • Подготавливаем действительный документ спецификации OpenAPI:
    • Инструкции по созданию документа спецификации OpenAPI с нуля см. В обзоре руководства по OpenAPI.
    • Для использования предварительно созданного документа в спецификации OpenAPI, можно использовать файл спецификации OpenWeatherMap или файл спецификации Sunrise/sunset API (Клик правой кнопкой мыши ссылку и сохраните файл YAML на рабочем столе.)
  • Нужно убедиться, что спецификация OpenAPI действительна. Для этого вставляем свой код спецификации OpenAPI в онлайн-редактор Swagger и видим, что слева не отображаются никакие предупреждения. Вид справа в редакторе Swagger показывает полностью функциональный дисплей Swagger UI.
  • Переходим в проект Swagger UI на GitHub
  • Нажмите Clone or download, а затем нажмите Download ZIP. Загрузите файлы в удобное место на вашем компьютере и распакуйте файлы.

Единственная папка, с которой мы будем работать в загруженном zip-архиве, – это папка dist (сокращение от дистрибутива). Все остальное используется, только если мы перекомпилируем файлы Swagger, что выходит за рамки этого руководства.

  • Извлечем папку dist из папки swagger-ui-master в другой каталог. (После этого папку swagger-ui-master и zip-файл можно удалить.)
  • Перетащим файл спецификации OpenAPI (из шага 1) в папку dist. (Если вы используете предварительно созданные файлы OpenAPI, файл называется либо openapi_openweathermap.yml, либо openapi_sunrise_sunset.yml.) Ваша файловая структура должна выглядеть следующим образом:

      ├── dist
      │   ├── favicon-16x16.png
      │   ├── favicon-32x32.png
      │   ├── index.html
      │   ├── oauth2-redirect.html
      │   ├── swagger-ui-bundle.js
      │   ├── swagger-ui-bundle.js.map
      │   ├── swagger-ui-standalone-preset.js
      │   ├── swagger-ui-standalone-preset.js.map
      │   ├── swagger-ui.css
      │   ├── swagger-ui.css.map
      │   ├── swagger-ui.js
      │   ├── swagger-ui.js.map
      │   ├── swagger30.yml
      │   └── [your openapi specification file]
    
  • В папке dist открываем index.html в текстовом редакторе, таком как Atom илиSublime Text.
  • Ищем следующий код:
url: "http://petstore.swagger.io/v2/swagger.json"
  • Меняем значение url на относительный путь к вашему файлу YAML, а затем сохраните файл. Например
url: "openapi_openweathermap.yml",

или

url: "openapi_sunrise_sunset.yml",
  • Изучим файл index.html локально в браузере. Стоит обратить внимание, что ограничения безопасности Chrome (возражения CORS) не позволяют просматривать файл Swagger UI локально. Но есть несколько обходных путей:
    • Просмотр файла локально с помощью Firefox (это самый простой способ);
    • Использование размещенного в Интернете URL-адреса openapi_openweathermap.yml или openapi_sunrise_sunset.yml. (Клик правой кнопкой мыши ссылку и выберите «Копировать адрес ссылки».);
    • Загрузка папки dist на веб-сервер и просмотр ее содержимого на сервере;
    • Поместить файл YAML в общедоступный GitHub Gist и затем нажать Raw. Использовать URL для этого Gist;
    • Использовать локальный сервер, такой как simple local HTTP server.

Когда файл Swagger UI будет готов к публикации, просто загружаем папку на веб-сервер и переходим в файл index.html. Например, если название каталога dist осталось без изменений, переходим по адресу http://myserver.com/dist/. (Имя папки dist можно менять на любое другое.)

Конфигурация параметров Swagger UI

Swagger UI предоставляет различные параметры конфигурации (не связанные с параметрами OpenAPI), которые можно использовать для настройки интерактивного дисплея. Например, можно указать, будет ли каждая конечная точка развернута или свернута, как будут сортироваться теги и операции, показывать ли заголовки запросов в ответе, включать ли раздел «Модели» после списка конечных точек и многое другое.

В этом руководстве не будем вдаваться в подробности этих параметров конфигурации.

Если посмотреть на код демонстрации пользовательского интерфейса Swagger (перейдите в View> Source), то увидим параметры, перечисленные в разделе // Build a system:

  // Build a system
const ui = SwaggerUIBundle({
  url: "openapi_openweathermap.yml",
  dom_id: '#swagger-ui',
  defaultModelsExpandDepth: -1,
  deepLinking: true,
  presets: [
    SwaggerUIBundle.presets.apis,
    SwaggerUIStandalonePreset
  ],
  plugins: [
    SwaggerUIBundle.plugins.DownloadUrl
  ],
  layout: "StandaloneLayout"
})

Все параметры (например, deepLinking, dom_id и т. Д.) являются значениями по умолчанию. Добавлен defaultModelsExpandDepth: -1, чтобы скрыть раздел «Модели» в нижней части экрана Swagger UI.

О параметрах конфигурации Swagger UI можете узнать в документации Swagger.

Проблемы Swagger UI

Изучая интерфейс Swagger, можно заметить несколько ограничений:

  • Не так много места для подробного описания работы конечных точек. При наличии нескольких абзацев с подробностями и сведениями о параметре, лучше всего ссылаться из описания на другую страницу в документации. Спецификация OpenAPI предоставляет способ ссылки на внешнюю документацию как в объекте paths, так и в объекте info, а также и в объекте externalDocs.
  • Интерфейс Swagger выглядит практически одинаково для каждого API. Можно настроить Swagger UI под свои собственные бренды, но понадобятся навыки UX. Относительно легко изменить цвет и изображение в верхней панели навигации.
  • Swagger UI может быть отдельным сайтом от другой документации. Это означает, что в своей документации потребуется ссылаться на Swagger в качестве ссылки для конечных точек. В разделе «Интеграция Swagger UI с остальными документами», описаны стратегии объединения справочных документов и руководства пользователя.

Устранение неполадок

При настройке Swagger UI, могут возникать проблемы. Следующие проблемы являются наиболее распространенными:

Проблема CORS

Если безопасность правильно настроена, но запросы отклоняются, это может быть связано с проблемой CORS (cross-origin resource sharing). CORS – это мера безопасности, которую веб-сайты внедряют, чтобы другие сценарии и процессы не могли получать свое содержимое через запросы от удаленных серверов. Подробности см. В разделе «Поддержка CORS» в документации по интерфейсу Swagger.

Если запросы не работают, переходим в консоль JavaScript браузера (в Chrome, View> Developer> Javascript Console), делаем запрос, и смотрим, относится ли ошибка к запросам разных источников. Если это так, можно попросить разработчиков включить CORS на конечных точках.

Проблемы с url хоста

Хост тестового сервера может быть еще одной причиной отклонения запросов. Некоторые API (например, Aeris Weather) требуют, создания идентификатор приложения на основе URL-адреса хоста, на котором будут выполняться запросы. Если зарегистрированным URL-адресом хоста является http://mysite.com, но тест отправляется по адресу https://editor.swagger.io/, сервер API отклонит запросы.

Встраивание Swagger UI в существующий сайт

Помимо публикации выходных данных пользовательского интерфейса Swagger в качестве отдельного сайта, можно встраивать файл Swagger в существующий сайт. Описание можно посмотреть здесь:

  • Standalone Swagger UI Demo
  • Embedded Swagger UI Demo

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

🔙

Go next ➡

Swagger UI предоставляет Фреймворк, который читает спецификацию OpenAPI. и создает веб-страницу с интерактивной документацией. В этом руководстве показано, как интегрировать документ спецификации OpenAPI в интерфейс Swagger.

Концептуальный обзор OpenAPI и Swagger можно посмотреть в разделе Знакомство со спецификациями OpenAPI и Swagger. Пошаговое руководство по созданию документа спецификации OpenAPI смотрим в Обзоре руководства OpenAPI 3.0.

Содержание раздела

Обзор Swagger UI

Знакомство со Swagger при помощи Petstore

  • Авторизация запроса

  • Создание запроса

  • Проверка создания питомца

Примеры сайтов с документаций по Swagger UI

Практическое занятие: Создание спецификации OpenAPI в Swagger UI

Конфигурация параметров Swagger UI

Проблемы Swagger UI

Устранение неполадок

Встраивание Swagger UI в существующий сайт

Обзор Swagger UI

Swagger UI – один из самых популярных инструментов для создания интерактивной документации. Swagger UI создает интерактивную консоль API для экспериментов с запросами в реальном времени. Кроме того, Swagger UI (активно управляемый проект с лицензией Apache 2.0) поддерживает последнюю версию спецификации OpenAPI (3.x) и интегрируется с другими инструментами Swagger.

Прежде чем мы углубимся в Swagger, нужно прояснить ключевые термины.

Swagger

Относится к инструментам API, связанным со спецификацией OpenAPI. Некоторыми из этих инструментов являются Swagger Editor, Swagger UI, Swagger Codegen, SwaggerHub и другие. Всеми инструментами управляет компания Smartbear. Для получения дополнительной информации см. Инструменты Swagger. «Swagger» являлся изначально оригинальным названием спецификации OpenAPI, но позже имя было изменено на OpenAPI, чтобы усилить открытый, не лицензионный характер стандарта. Люди иногда ссылаются на оба имени взаимозаменяемо (особенно на старых веб-страницах), но «OpenAPI» – это то, как следует обращаться к спецификации. Дополнительные сведения о разнице между OpenAPI и Swagger см. В разделе «В чем разница между Swagger и OpenAPI?».

OpenAPI

Официальное название спецификации OpenAPI. Спецификация OpenAPI предоставляет набор свойств, которые можно использовать для описания REST API. Рабочий, валидный документ можно использовать для создания интерактивной документации, создания клиентских SDK, запуска модульных тестов и многого другого. Подробности спецификации можно изучить на GitHub по адресу https://github.com/OAI/OpenAPI-Specification. В рамках инициативы Open API с Linux Foundation спецификация OpenAPI направлена ​​на то, чтобы быть независимой от производителя (многие компании участвуют в ее разработке).

Swagger Editor

Онлайн-редактор, который проверяет документацию OpenAPI на соответствие правилам спецификации OpenAPI. Редактор Swagger помечает ошибки и дает советы по форматированию.

Swagger UI

Веб-фрэймворк (на GitHub), который анализирует документ в спецификации OpenAPI и создает веб-страницу интерактивной документации. Swagger UI – это инструмент, который превращает спецификацию в подобный Petstore-сайт.

Swagger Codegen

Генерирует код SDK для множества различных платформ (таких как Java, JavaScript, Scala, Python, PHP, Ruby, Scala и другие). Код SDK помогает разработчикам интегрировать API на конкретной платформе и обеспечивает более надежные реализации, которые могут включать в себя больше масштабирования, многопоточности и т.д.. В общем, SDK – это наборы инструментов для реализации запросов, сделанных с помощью API. Swagger Codegen генерирует клиентские SDK практически на каждом языке программирования. См. Swagger Codegen для получения дополнительной информации. Смотрите также SDK и примеры приложений.

Знакомство со Swagger при помощи Petstore

Чтобы лучше понять интерфейс Swagger, давайте рассмотрим пример Swagger Petstore. В примере Petstore сайт генерируется с помощью Swagger UI.

Petstore

Конечные точки сгруппированы следующим образом:

  • pet
  • store
  • user

Авторизация запроса

Прежде чем делать какие-либо запросы, нужна авторизация. Нажимаем кнопку Authorize и заполняем информацию, требуемую в окне «Авторизация», изображенном ниже:

Authorize

Пример Petstore имеет модель безопасности OAuth 2.0. Код авторизации только для демонстрационных целей. Нет никакой реальной логики авторизации этих запросов, поэтому просто закрываем окно Авторизации.

Создание запроса

Теперь создадим запрос:

  1. Развернем конечную точку POST Pet
  2. Нажимаем кнопку Try it out

Try it out

После того, как мы нажмем кнопку Try it out, значение примера в поле «Тело запроса» станет редактируемым.

  1. В поле «Example Value» изменяем первое значение id на случайное целое число, например 193844. Также значение второго name на другое (имя вашего питомца).
  2. Нажимаем Execute

Execute

Выполнение примера запроса Petstore

Пользовательский интерфейс Swagger отправляет запрос и показывает отправленный curl. Раздел Ответы показывает ответ. (Если выбрать JSON вместо XML в раскрывающемся списке «Response content type», формат ответа будет показан в формате JSON.)

Response

!Важно! Petstore – это функционирующий API, и вы фактически создали питомца. Теперь вам нужно взять на себя ответственность за вашего питомца и начать кормить и ухаживать за ним! Шутки, шутками, но большинство пользователей не осознают, что играют с реальными данными, когда выполняют ответы в API (особенно при использовании своего собственного ключа API). Эти тестовые данные могут быть чем-то, что вам придется стереть при переходе от исследований и изучения API к использованию API для реального использования.

Проверка создания питомца

  1. Разворачиваем точку GET /pet/{petId}
  2. Нажимаем кнопку Try it out
  3. Вводим ID питомца, который использовали в предыдущей операции. (Если забыли ID, посмотрите на конечную точку POST Pet, чтобы проверить значение.)
  4. Нажимаем Execute . В ответе мы должны увидеть имя нашего питомца.

Примеры сайтов с документаций по Swagger UI

Прежде чем мы перейдем к другому API с этим пособием по Swagger (кроме демонстрации Petstore), посмотрим на другие реализации Swagger:

  • Reverb
  • VocaDB
  • Watson Developer Cloud
  • The Movie Database API
  • Zomato API

Некоторые из этих сайтов выглядят одинаково, но другие, такие как The Movie Database API и Zomato, были легко интегрированы в остальную часть их сайта документации.

Глядя на примеры, можно заметить краткость документации в реализации Swagger. Эта краткость объясняется тем, что дисплей Swagger предназначен для интерактивного взаимодействия, где можно опробовать вызовы и посмотреть ответы – используя свой собственный ключ API, чтобы увидеть свои собственные данные. такой подход получил название: «учись, практикуясь». Кроме того, Swagger UI охватывает только адресные темы документации. Концептуальные разделы обычно рассматриваются в отдельном руководстве.

👨‍💻 Практическое занятие: Создание спецификации OpenAPI в Swagger UI

На этом занятии мы создадим документацию в Swagger UI в спецификации OpenAPI. Если вы используете один из предварительно созданных файлов OpenAPI, вы можете увидеть демонстрацию того, что мы создадим здесь: OpenWeatherMap Swagger UI или Sunrise/sunset Swagger UI).

OpenWeatherMap

Для интеграции спецификации OpenAPI в Swagger UI:

  1. Подготавливаем действительный документ спецификации OpenAPI:
  • Инструкции по созданию документа спецификации OpenAPI с нуля см. В обзоре руководства по OpenAPI.
  • Для использования предварительно созданного документа в спецификации OpenAPI, можно использовать файл спецификации OpenWeatherMap или файл спецификации Sunrise/sunset API (Клик правой кнопкой мыши ссылку и сохраните файл YAML на рабочем столе.)
  1. Нужно убедиться, что спецификация OpenAPI действительна. Для этого вставляем свой код спецификации OpenAPI в онлайн-редактор Swagger и видим, что слева не отображаются никакие предупреждения. Вид справа в редакторе Swagger показывает полностью функциональный дисплей Swagger UI.
  2. Переходим в проект Swagger UI на GitHub
  3. Нажмите Clone or download, а затем нажмите Download ZIP. Загрузите файлы в удобное место на вашем компьютере и распакуйте файлы.

Единственная папка, с которой мы будем работать в загруженном zip-архиве, – это папка dist (сокращение от дистрибутива). Все остальное используется, только если мы перекомпилируем файлы Swagger, что выходит за рамки этого руководства.

  1. Извлечем папку dist из папки swagger-ui-master в другой каталог. (После этого папку swagger-ui-master и zip-файл можно удалить.)

  2. Перетащим файл спецификации OpenAPI (из шага 1) в папку dist. (Если вы используете предварительно созданные файлы OpenAPI, файл называется либо openapi_openweathermap.yml, либо openapi_sunrise_sunset.yml.) Ваша файловая структура должна выглядеть следующим образом:

     ├── dist
     │   ├── favicon-16x16.png
     │   ├── favicon-32x32.png
     │   ├── index.html
     │   ├── oauth2-redirect.html
     │   ├── swagger-ui-bundle.js
     │   ├── swagger-ui-bundle.js.map
     │   ├── swagger-ui-standalone-preset.js
     │   ├── swagger-ui-standalone-preset.js.map
     │   ├── swagger-ui.css
     │   ├── swagger-ui.css.map
     │   ├── swagger-ui.js
     │   ├── swagger-ui.js.map
     │   ├── swagger30.yml
     │   └── [your openapi specification file]
    
  3. В папке dist открываем index.html в текстовом редакторе, таком как Atom илиSublime Text.

  4. Ищем следующий код:
    url: “http://petstore.swagger.io/v2/swagger.json”,

  5. Меняем значение url на относительный путь к вашему файлу YAML, а затем сохраните файл. Например

url: "openapi_openweathermap.yml",

или

url: "openapi_sunrise_sunset.yml",
  1. Изучим файл index.html локально в браузере. Стоит обратить внимание, что ограничения безопасности Chrome (возражения CORS) не позволяют просматривать файл Swagger UI локально. Но есть несколько обходных путей:
  • Просмотр файла локально с помощью Firefox (это самый простой способ);
  • Использование размещенного в Интернете URL-адреса openapi_openweathermap.yml или openapi_sunrise_sunset.yml. (Клик правой кнопкой мыши ссылку и выберите «Копировать адрес ссылки».);
  • Загрузка папки dist на веб-сервер и просмотр ее содержимого на сервере;
  • Поместить файл YAML в общедоступный GitHub Gist и затем нажать Raw. Использовать URL для этого Gist;
  • Использовать локальный сервер, такой как simple local HTTP server.

Когда файл Swagger UI будет готов к публикации, просто загружаем папку на веб-сервер и переходим в файл index.html. Например, если название каталога dist осталось без изменений, переходим по адресу http://myserver.com/dist/. (Имя папки dist можно менять на любое другое.)

Для получения дополнительных инструкций по работе с Swagger UI см. Документацию Swagger.io.

Конфигурация параметров Swagger UI

Swagger UI предоставляет различные параметры конфигурации (не связанные с параметрами OpenAPI), которые можно использовать для настройки интерактивного дисплея. Например, можно указать, будет ли каждая конечная точка развернута или свернута, как будут сортироваться теги и операции, показывать ли заголовки запросов в ответе, включать ли раздел «Модели» после списка конечных точек и многое другое.

В этом руководстве не будем вдаваться в подробности этих параметров конфигурации.

Если посмотреть на код демонстрации пользовательского интерфейса Swagger (перейдите в View> Source), то увидим параметры, перечисленные в разделе // Build a system:

  // Build a system
const ui = SwaggerUIBundle({
  url: "openapi_openweathermap.yml",
  dom_id: '#swagger-ui',
  defaultModelsExpandDepth: -1,
  deepLinking: true,
  presets: [
    SwaggerUIBundle.presets.apis,
    SwaggerUIStandalonePreset
  ],
  plugins: [
    SwaggerUIBundle.plugins.DownloadUrl
  ],
  layout: "StandaloneLayout"
})

Все параметры (например, deepLinking, dom_id и т. Д.) являются значениями по умолчанию. Добавлен defaultModelsExpandDepth: -1, чтобы скрыть раздел «Модели» в нижней части экрана Swagger UI.

О параметрах конфигурации Swagger UI можете узнать в документации Swagger.

Проблемы Swagger UI

Изучая интерфейс Swagger, можно заметить несколько ограничений:

  • Не так много места для подробного описания работы конечных точек. При наличии нескольких абзацев с подробностями и сведениями о параметре, лучше всего ссылаться из описания на другую страницу в документации. Спецификация OpenAPI предоставляет способ ссылки на внешнюю документацию как в объекте paths, так и в объекте info, а также и в объекте externalDocs.
  • Интерфейс Swagger выглядит практически одинаково для каждого API. Можно настроить Swagger UI под свои собственные бренды, но понадобятся навыки UX. Относительно легко изменить цвет и изображение в верхней панели навигации.
  • Swagger UI может быть отдельным сайтом от другой документации. Это означает, что в своей документации потребуется ссылаться на Swagger в качестве ссылки для конечных точек. В разделе «Интеграция Swagger UI с остальными документами», описаны стратегии объединения справочных документов и руководства пользователя.

Устранение неполадок

При настройке Swagger UI, могут возникать проблемы. Следующие проблемы являются наиболее распространенными:

Проблема CORS

Если безопасность правильно настроена, но запросы отклоняются, это может быть связано с проблемой CORS (cross-origin resource sharing). CORS – это мера безопасности, которую веб-сайты внедряют, чтобы другие сценарии и процессы не могли получать свое содержимое через запросы от удаленных серверов. Подробности см. В разделе «Поддержка CORS» в документации по интерфейсу Swagger.

Если запросы не работают, переходим в консоль JavaScript браузера (в Chrome, View> Developer> Javascript Console), делаем запрос, и смотрим, относится ли ошибка к запросам разных источников. Если это так, можно попросить разработчиков включить CORS на конечных точках.

Проблемы с url хоста

Хост тестового сервера может быть еще одной причиной отклонения запросов. Некоторые API (например, Aeris Weather) требуют, создания идентификатор приложения на основе URL-адреса хоста, на котором будут выполняться запросы. Если зарегистрированным URL-адресом хоста является http://mysite.com, но тест отправляется по адресу https://editor.swagger.io/, сервер API отклонит запросы.

Встраивание Swagger UI в существующий сайт

Помимо публикации выходных данных пользовательского интерфейса Swagger в качестве отдельного сайта, можно встраивать файл Swagger в существующий сайт. Описание можно посмотреть здесь:

  • Standalone Swagger UI Demo
  • Embedded Swagger UI Demo

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

🔙

Go next ➡

Добавить комментарий