Уровень сложности
Средний
Время на прочтение
41 мин
Количество просмотров 14K
В современных реалиях разработки программного обеспечения бывает достаточно трудно быстро и качественно написать техническую документацию к проекту, особенно когда данному процессу уделяется недостаточно времени по разным причинам: от временных ограничений, до индивидуальной непереносимости монотонного процесса участниками проектной группы, который, порой, может тормозить рабочие процессы.
Уже сейчас существуют инструменты, которые позволяют ускорить процесс документирования программного обеспечения. Эти инструменты значительно облегчают задачи разработчиков по ведению технической документации и внесению быстрых правок, что позволяет отражать в документации актуальные положения фактического поведения системы. В данной статье будет рассмотрен один из таких инструментов.
Основное внимание в статье будет уделено автоматизации процесса создания документации 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 включает в себя следующие наиболее используемые инструменты:
-
Swagger Editor — редактор для разработки API-интерфейсов в соответствии со спецификацией Open API
-
Swagger UI — веб-приложение, позволяющая визуализировать определения спецификаций Open API в интерактивном пользовательском интерфейсе
-
Swagger Codegen — создание серверных заглушек и клиентских SDK-пакетов на основе определений спецификаций Open API
Этим списком весь набор Swagger не заканчивается, их достаточно много. Читателю предлагается ознакомиться со всем набором инструментов на официальном сайте.
Из вышеприведённого списка наиболее интересным инструментом является Swagger Editor, поскольку он предоставляет интерфейс для создания файла документации по спецификации Open API вручную.
Однако, перед началом рассмотрения инструмента Swagger Editor следует ответить на вопрос, а что же мы, собственно, собираемся документировать? API слишком общее понятие, следует конкретизировать что именно мы собираемся документировать. И в этом может помочь архитектурный паттерн Controller Service Repository.
Controller Service Repository
Controller Service Repository (CSR) — архитектурный паттерн, который помогает разделить ответственность между слоями приложения и соблюдать принципы SOLID.
Само определение паттерна содержит три важных элемента:
-
Controller — слой классов, отвечающих за обработку запросов (в частности — пользовательских). В данных классах происходит получение запроса от клиента (request), валидация входных данных (если она подразумевалась), передача данных конкретному сервису, обработка ошибок и формирование ответа пользователю (response).
-
Service — слой классов, отвечающих за бизнес-логику приложения. В сервисах отсутствуют прямые обращения к базе данных — данная функция делегируется репозиториям, которые завершают это перечисление.
-
Repository — слой классов, отвечающих за сохранение и извлечение некоторого набора данных. В репозиториях не должно быть бизнес-логики. В методах репозитория должны лишь формироваться и выполняться запросы к базе данных.
Удобно представлять эти слои в виде сферы из разных слоёв, каждый из которых по цепочки находится «ближе к данным» (постепенное уменьшение уровня абстракции над взаимодействием с базой данных).
На рисунке ниже представлена данная модель.
Причём здесь данный архитектурный паттерн? Дело в том, что данный паттерн является одним из самых популярных на сегодняшний день и отлично подходит для объяснения момента, с которого начинается документирование API (определение этой границы).
Все API, как правило, определяются в объединении множества маршрутов и конкретных обработчиков в слое Controller (иными словами — привязка конкретного метода из любого класса слоя Controller, к конкретному маршруту).
Следовательно, в слое Controller и происходит документирование API. Возможны комбинации, но в основном это можно принять как правило. Это будет рассмотрено на практике более детально.
Swagger Editor
Как уже ранее упоминалось Swagger Editor позволяет визуализировать документацию в соответствии с описанием по спецификации OpenAPI.
Каким образом представлено данное описание? В формате YAML. Грубо говоря вся документация проекта будет представлена в виде одного файла с расширением yaml. Это очень удобно, ведь при необходимости этот документ можно перенести куда угодно, при этом он будет однозначно интерпретирован рассматриваемым набором инструментов, т.к. существует единый стандарт (а в случае проблем с соответствием стандарту инструменты будут выдавать сообщения об ошибках).
На рисунке ниже представлена работа Swigger Editor.
Данные, представленные в формате YAML, можно изменять и тогда в Swagger Editor будут отображаться все изменения.
Теоретически можно самостоятельно описать все API в своём сервисе с помощью данного инструмента и просто передавать файл с расширением yaml между разработчиками, чтобы они сами у себя запускали веб-приложение Swagger UI и указывали источником данных этот файл, однако данный подход крайне неудобен. Даже сейчас файл документации содержит порядка 800 строк, что довольно громоздко для количества маршрутов и их сложности, представленных в качестве базового примера в Swagger Editor.
Как можно решить проблему с ручным документированием API? Автоматизировать данный процесс. Тут мы приступаем к рассмотрению практической части.
Определение функциональных требований к сервисам
Всего будет два сервиса, которые схожи между собой функционалом. Предположим, что перед нами была поставлена задача разработать сервис авторизации пользователя.
Один сервис будет использовать технологический стек Node.js, Express.js, JavaScript, а другой — Gin, Golang. Первый сервис будет иметь кодовое название NEJ, а второй — GG.
Опустим момент с моделированием базы данных каждого сервиса, что рекомендуется своевременно делать при более серьёзной разработке подобного рода сервисов.
Приступим к разработке сервиса с кодовым названием NEJ.
Документирование сервиса NEJ
Для начала изучим файловую структуру проекта.
В файловой структуре проекта определены следующие директории и файлы (только основные элементы):
-
config — директория, содержащая конфигурационные файлы сервиса
-
constants — директория с общими константами
-
controllers — директория, содержащая контроллеры (слой Controller)
-
db — директория с настройками подключения к базе данных и определением моделей
-
docs — документация сервиса (*.yaml)
-
dtos — директория, содержащая DTO
-
exceptions — директория, содержащая основные ошибки сервиса (классы для обработки ошибок)
-
logger — настройки логгера
-
logs — логи сервиса
-
middlewares — промежуточное программное обеспечение (используется для проверки токенов JWT и обработки исключений)
-
routers — директория, содержащая конкретные привязки методов контроллеров с конкретными маршрутами (url-адресами)
-
services — директория, содержащая сервисы (слой Services)
-
utils — директория с утилитами
-
*.env — файлы с переменными окружения
-
generate-doc.js — скрипт, для автоматической генерации документации API
-
index.js — точка входа в серверное приложение
-
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.
Как видно из рисунка всё действительно работает, более того — отображается документация в спецификации OpenAPI 3.
Внимательный читатель может отметить в коде точки входа, что есть отдельный опциональный блок, который позволяет увидеть документацию в более ранней спецификации (OpenAPI 2).
Однако, зачем это необходимо? Ответ прост — демонстрация исходной документации Swagger, которая была первоначально сгенерирована. Дело в том, что документация по умолчанию генерируется по спецификации OpenAPI 2, т.к. это особенности используемого пакета.
Для автоматической генерации документации по контроллерам был использован пакет express-swagger-generator, который не пользуется большой популярностью в статьях подобного характера (Swagger, Express.js).
Данный пакет отлично справился с задачей автоматической генерации документации. Ссылка на официальную документацию.
Приступим к обзору скрипта, который производит автоматическую генерацию документации в спецификацию 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
Используемые пакеты
Все используемые пакеты в сервисе 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-комментарии, однако их интерпретация используемым пакетом осуществляется по своему.
Дадим разъяснения данным комментариям:
-
В первой строке многострочного комментария представлено описание конкретного API
-
@ route POST /auth/sign-up — привязка API к текущему адресу в документации
-
@ group Авторизация … — соотнесение данного API к конкретной группе (аналогично, что и tag)
-
@ param {SignUpDto.model} input.body.required — определение модели входных данных (без необходимости в ссылках #ref, по спецификации OpenAPI)
-
@ returns {AuthDto.model} 200 — описание модели выходных данных (при успешной обработке запроса)
-
@ 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
*/
Дадим пояснение данному определению:
-
@ typedef SignUpDto — определение модели (схемы) SignUpDto (на которую потом можно будет ссылаться)
-
@ property {string} email.required — определение параметра email, с типом string
-
Остальное — по аналогии со вторым элементом из списка
Также можно сделать более сложное определение:
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;
Дадим пояснения тому, что есть в многострочном комментарии:
-
@ typedef AuthDto — создание модели AuthDto
-
@ property {TokenDto.model} tokens.required — создание параметра в модели, которое по сути является другой моделью (вложенность схем)
-
Далее — по аналогии
Необязательно многострочные комментарии с определением моделей держать рядом с моделями, можно держать их и отдельно. Например, следующее определение будет работать корректно:
/**
* @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
Приведу пояснения по файловой структуре проекта (только основные элементы):
-
cmd — директория, в которой определена точка входа в серверное приложение (main.go)
-
config — директория с файлами конфигурации
-
database — директория, в которой содержатся схемы и дампы базы данных
-
docs — директория, в которой располагается сгенерированная документация
-
logs — логи сервера
-
pkg — основные пакеты сервиса
-
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.
В директории присутствует каталог 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:
-
@ Summary — название запроса в документации
-
@ Tags — теги для группировки API
-
@ Description — описание запроса в документации
-
@ ID — идентификатор API
-
@ Accept — данные, которые будут приниматься на входе запроса
-
@ Produce — формат данных, которые будут возвращаться в ответе
-
@ Param input body userModel.UserSignInModel — параметры запроса
-
@ Success 200 {object} userModel.TokenAccessModel «data» — код ответа, формат данных и описание
-
@ Failure 400,404 {object} httpModel.ResponseMessage — код ответа, формат данных и описание
-
@ 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))
При запуске сервиса мы получим возможность просмотра документации онлайн.
Вывод
В рамках данной статьи были описаны два сервиса, которые использовали инструментарий Swagger и дополнительные пакеты, с целью достижения автоматического документирования своих API, с чем успешно справились.
Был предложен ряд пакетов, которые можно использовать для решения этой задачи как на Express.js (swagger-express-generate, swagger-ui-express), так и на Gin (swag). Читатель может обратиться к исходному коду и основываясь на данных примерах реализовать документирование API на своих сервисах.
Используя инструментарий Swagger можно значительно ускорить процесс создания документации API сервисов, не нагромождая при этом код лишними параметрами спецификации OpenAPI.
Ссылки на использованные источники
-
Сервис NEJ
-
Сервис GG
-
Controller Service Repository
-
Шаблон для работы с swagger-express-generate
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.
Конечные точки сгруппированы следующим образом:
- pet
- store
- user
Авторизация запроса
Прежде чем делать какие-либо запросы, нужна авторизация. Нажимаем кнопку Authorize и заполняем информацию, требуемую в окне «Авторизация», изображенном ниже:
Пример Petstore имеет модель безопасности OAuth 2.0. Код авторизации только для демонстрационных целей. Нет никакой реальной логики авторизации этих запросов, поэтому просто закрываем окно Авторизации.
Создание запроса
Теперь создадим запрос:
- Разворачиваем конечную точку POST Pet
- Нажимаем кнопку
Try it out
После того, как мы нажмем кнопку Try it out
, значение примера в поле «Тело запроса» станет редактируемым.
- В поле «Example Value» изменяем первое значение
id
на случайное целое число, например193844
. Также значение второгоname
на другое (имя вашего питомца). - Нажимаем
Execute
.
Пользовательский интерфейс Swagger отправляет запрос и показывает отправленный curl. Раздел Ответы показывает ответ. (Если выбрать JSON вместо XML в раскрывающемся списке «Response content type», формат ответа будет показан в формате JSON.)
Проверка создания питомца
- Разворачиваем точку GET /pet/{petId}
- Нажимаем кнопку
Try it out
- Вводим ID питомца, который использовали в предыдущей операции. (Если забыли ID, посмотрите на конечную точку POST Pet, чтобы проверить значение.)
- Нажимаем
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).
Для интеграции спецификации 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 ➡
NOTE: Swagger Core 2.X produces OpenApi 3.0 definition files.
If you’re looking for swagger 1.5.X and OpenApi 2.0, please refer to 1.5.X JAX-RS Setup
NOTE: Since version 2.2.0 Swagger Core supports OpenAPI 3.1; see this page for details
NOTE: Jakarta namespace support (since version 2.1.7)
Since version 2.1.7 Swagger Core supports also Jakarta namespace, with a parallel set of artifacts with -jakarta
suffix, providing the same functionality as the «standard» javax
namespace ones.
While behaviour described in this documentation is the same for both namespaces, artifact IDs, JEE / Jakarta EE versions and Jackson versions mentioned
refer to javax
namespace.
If you are using jakarta namespace:
- when you read artifact IDs in the form:
swagger-*
(e.g.swagger-core
), replace them withswagger-*-jakarta
(e.g.swagger-core-jakarta
) - when you read
javax.*
in package names, replace that withjakarta
(e.gjakarta.ws.rs.GET
) - when JEE / Jakarta EE dependencies are provided in examples, replace their version with Jakarta EE 9 versions.
- When Jackson dependencies are provided in examples, add the
jakarta
classifier for artifacts supporting it. See Jackson release notes
Jakarta
namespace Swagger Core artifacts need Jackson 2.12+
What is it about?
NOTE: swagger-core
is based on OpenAPI specification; check out related docs for an overview of Swagger ecosystem.
swagger-core
is an open source Java implementation of Swagger/OpenAPI, providing:
-
swagger-models
: OpenAPI specification Java implementation -
swagger-core
: resolves (annotated) java POJOs into OpenAPI schemas, handles serialization/deserialization and provides an integration mechanism. -
swagger-jaxrs2
: resolves JAX-RS (annotated) resources into an OpenAPI definition, and provides an integration mechanism. -
swagger-annotations
: a set of annotations to declare and manipultate output generated byswagger-core
,swagger-jaxrs2
and/or other projects. -
swagger-maven-plugin
(since 2.0.5): provides a maven plugin to resolve an OpenAPI definition at build time (usingswagger-jaxrs2
). Please see module readme -
swagger-gradle-plugin
(since 2.0.5): provides a gradle plugin to resolve an OpenAPI definition at build time (usingswagger-jaxrs2
). Please see module readme
Where to start
One of the common usage scenarios is to integrate swagger-jaxrs2
into an existing or new JAX-RS based project («code-first»),
to automatically provide and expose its APIs definition, which is kept in sync during the project lifecycle.
Such definition can be the base for further processing/consumption, including API documentation
(e.g with swagger-ui, API client generation in various languages
(e.g with swagger-codegen), custom processing, and so on.
Such result is achieved by scanning JAX-RS resources and resolving their operations and used types, (also)
processing applied annotations (e.g. Swagger, JAX-RS, Jackson, JAXB, etc.).
An extension mechanism allows to further customize and pre/post processing result.
Check out Quick start below, or jump to Integration and configuration.
Note: Quick start and related sections detail the steps needed to integrate swagger at runtime; since version 2.0.5, swagger-core
project also provides maven and gradle plugins to resolve an OpenAPI definition at build time. As these are based on the same resolving and configuration mechanisms, most information in the wiki applies also in this case.
Quick start
Swagger uses maven for build and deployment and its artifacts are available at Maven Central.
You can use the maven dependencies with any dependency management system that supports maven dependencies such as Maven,
Ivy and Gradle. If you’re not using Maven, please refer to Not using Maven
Integrating swagger-core
into a JAX-RS application can be as easy as adding its dependency to the project POM:
<dependencies> ... <dependency> <groupId>io.swagger.core.v3</groupId> <artifactId>swagger-jaxrs2</artifactId> <version>2.2.7</version> </dependency> <dependency> <groupId>io.swagger.core.v3</groupId> <artifactId>swagger-jaxrs2-servlet-initializer</artifactId> <version>2.2.7</version> </dependency> </dependencies>
or since version 2.1.2
better (see #3412):
<dependencies> ... <dependency> <groupId>io.swagger.core.v3</groupId> <artifactId>swagger-jaxrs2</artifactId> <version>2.2.7</version> </dependency> <dependency> <groupId>io.swagger.core.v3</groupId> <artifactId>swagger-jaxrs2-servlet-initializer-v2</artifactId> <version>2.2.7</version> </dependency> </dependencies>
Consider a simple scenario, consisting in a JAX-RS application (e.g. Jersey or RESTEasy) with a resource like:
@Path("/pet") @Produces({"application/json", "application/xml"}) public class PetResource { @GET @Path("/{petId}") public Pet getPetById(@PathParam("petId") Long petId) throws io.swagger.sample.exception.NotFoundException { // return pet return new Pet(); } @POST @Consumes("application/json") public Response addPet( @Parameter(description = "Pet object that needs to be added to the store", required = true) Pet pet) { // add pet return Response.ok().entity("SUCCESS").build(); } }
and related model POJO:
@XmlRootElement(name = "Pet") public class Pet { private long id; private String name; private List<Tag> tags = new ArrayList<Tag>(); @XmlElement(name = "id") public long getId() { return id; } public void setId(long id) { this.id = id; } @XmlElement(name = "name") public String getName() { return name; } public void setName(String name) { this.name = name; } @XmlElementWrapper(name = "tags") @XmlElement(name = "tag") public List<Tag> getTags() { return tags; } public void setTags(List<Tag> tags) { this.tags = tags; } }
Just by adding the dependencies, an endpoint <server_url>/<application_path>/openapi.json
(and openapi.yaml
) is activated,
exposing the OpenAPI definition of the app APIs serialized as json or yaml, as resolved by swagger-core
processing JAX-RS resources defined in the application.
NOTE: The endpoint is fully configurable and/or the definition endpoint can be provided by application defined resources. See Integration and configuration
Given the resource above, the resolved spec looks like:
openapi: 3.0.1 paths: /sample/pet/{petId}: get: operationId: getPetById parameters: - name: petId in: path required: true schema: type: integer format: int64 responses: default: description: default response content: application/json: schema: $ref: '#/components/schemas/Pet' application/xml: schema: $ref: '#/components/schemas/Pet' /sample/pet: post: operationId: addPet requestBody: description: Pet object that needs to be added to the store content: application/json: schema: $ref: '#/components/schemas/Pet' required: true responses: default: description: default response content: application/json: {} application/xml: {} components: schemas: Tag: type: object properties: id: type: integer format: int64 name: type: string xml: name: Tag Pet: type: object properties: id: type: integer format: int64 name: type: string tags: type: array xml: wrapped: true items: $ref: '#/components/schemas/Tag' xml: name: Pet
In this case resources are identified and provided to swagger-core
engine by the swagger-jaxrs2-servlet-initializer
; there are however
several scenarios in which the dependency to swagger-jaxrs2-servlet-initializer
(or swagger-jaxrs2-servlet-initializer-v2
, see above) is not necessary, as Swagger integration mechanism is capable
of identifying resources from the ones configured by the JAX-RS environment, even without swagger specific settings (e.g. Application.getClasses()
, resourcePackages
Jersey init parameter, and more.
For example, adding io.swagger.v3.jaxrs2.integration.resources
to Jersey 2 container servlet/filter jersey.config.server.provider.packages
init param is by itself sufficient to integrate Swagger and have it scan and expose resolved spec. (in servlet-context-path/openapi.json
or servlet-context-path/openapi.yaml
).
Refer to Dependencies and Exposing OpenAPI definition for more details.
Resources configuration however, along with other init/config settings are fully configurable,
see below and Integration and configuration.
Try it out by cloning/downloading the
Jersey or
RESTEasy sample,
and running it with mvn package jetty:run
OpenAPI definition will be available at http://localhost:8002/sample/openapi.yaml
(and .json
).
Not too bad so far, at this point we have a dynamic always-in-sync OpenAPI definition matching
our application APIs, basically with zero code changes.
From here we might further customize our result by:
- Providing your own configuration (e.g resource packages/classes, output settings, etc.)
- Taking full control of your API definition (using Swagger annotations)
Or you can allow anyone — be it your development team or your end consumers — to visualize and
interact with the API’s resources without having any of the implementation logic in place, using swagger-ui.
Skip to Integration and configuration for full configuration details and integration scenarios.
Your own configuration
There are several ways to provide configuration; probably the easiest and least intrusive way is
adding a yaml (or json) file named openapi.yaml
or openapi-configuration.yaml
to the classpath of your application (location and path are flexible and configurable), e.g.
resourcePackages: - io.swagger.sample.resource prettyPrint: true cacheTTL: 0 openAPI: info: version: '1.0' title: Swagger Pet Sample App Config File description: 'This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.' termsOfService: http://swagger.io/terms/ contact: email: apiteam@swagger.io license: name: Apache 2.0 url: http://www.apache.org/licenses/LICENSE-2.0.html
In the example above, we are providing the packages to be considered while resolving the definition
(resourcePackages
), we declare we want a pretty printed output (prettyPrint
), we disable the cache to
resolve the definition each time the endpoint is hit (cacheTTL
), and we provide an info section directly in
OpenAPI format, which will be merged with the resolved definition (openAPI
).
In this case, as we are providing resourcePackages
ourselves, there is no need to include the initializer
dependency,
therefore a single dependency on swagger-jaxrs2
is sufficient:
<dependencies> ... <dependency> <groupId>io.swagger.core.v3</groupId> <artifactId>swagger-jaxrs2</artifactId> <version>2.0.0</version> </dependency> </dependencies>
Try it out by cloning/downloading the
Jersey or
RESTEasy sample,
and running it with mvn package jetty:run
OpenAPI definition will be available at http://localhost:8002/sample/openapi.yaml
(and .json
).
Full description of configuration properties is available here
Full control of your API definition
While Swagger resolver mechanism is able to analyze resource classes structure and various annotations
(e.g JAX-RS, Jackson, etc.) there are cases where metadata is simply not available (for example determining
the response schema of an operation, when the resource method is returning an instance
of JAX-RS Response
instead of a model POJO) and/or we want to completely customize the definition.
To handle this and other cases, and to be able to have full control over the resolved API definition,
usage of Swagger annotations comes handy.
Further customization can also be achieved by extension mechanisms.
Note: swagger-jaxrs2 reader engine includes by default also methods of scanned resources which are not annotated with @Operation,
as long as a jax-rs @Path is defined at class and/or method level, together with the http method annotation (@GET, @POST, etc).
This behaviour is controlled by configuration property readAllResources
which defaults to true. By setting this flag to
false only Operation
annotated methods are considered.
Let’s slightly modify our simple resource:
@Path("/pet") @Produces({"application/json", "application/xml"}) public class PetResource { static PetData petData = new PetData(); @GET @Path("/{petId}") @Operation(summary = "Find pet by ID", tags = {"pets"}, description = "Returns a pet when 0 < ID <= 10. ID > 10 or nonintegers will simulate API error conditions", responses = { @ApiResponse(description = "The pet", content = @Content( schema = @Schema(implementation = Pet.class) )), @ApiResponse(responseCode = "400", description = "Invalid ID supplied"), @ApiResponse(responseCode = "404", description = "Pet not found") }) public Response getPetById( @Parameter( description = "ID of pet that needs to be fetched", schema = @Schema( type = "integer", format = "int64", description = "param ID of pet that needs to be fetched", allowableValues = {"1","2","3"} ), required = true) @PathParam("petId") Long petId) throws io.swagger.sample.exception.NotFoundException { Pet pet = petData.getPetById(petId); if (null != pet) { return Response.ok().entity(pet).build(); } else { throw new io.swagger.sample.exception.NotFoundException(404, "Pet not found"); } } }
Here we explicitly declare responses for the operation,
using @ApiResponse annotation; additionally we define operation tags and
description and we provide a custom schema for a parameter.
The updated definition looks like:
... paths: /sample/pet/{petId}: get: tags: - pets summary: Find pet by ID description: Returns a pet when 0 < ID <= 10. ID > 10 or nonintegers will simulate API error conditions operationId: getPetById parameters: - name: petId in: path description: ID of pet that needs to be fetched required: true schema: type: integer description: param ID of pet that needs to be fetched format: int64 enum: - 1 - 2 - 3 responses: default: description: The pet content: application/json: schema: $ref: '#/components/schemas/Pet' application/xml: schema: $ref: '#/components/schemas/Pet' 400: description: Invalid ID supplied 404: description: Pet not found ...
We have been able to define a set of responses for the operation and customize a parameter schema, by adding related annotations.
Annotations can be applied to resource classes and whatever model POJOs (particularly used in this context is the @Schema annotation).
Try it out by cloning/downloading the
Jersey or
RESTEasy sample,
and running it with mvn package jetty:run
OpenAPI definition will be available at http://localhost:8002/sample/openapi.yaml
(and .json
).
A Petstore sample exemplifies usage of all annotations
Full description of Swagger annotations is available here
Visualize and interact
Once an API definition is available, you might want to visualize it in a nice UI, and interact with it, for example
testing the endpoint with an actual call. Such functionality is provided by swagger-ui which is nicely integratable with swagger-core
You can see how integration works by cloning/downloading the
Jersey or
RESTEasy sample (or most of samples),
and running it with mvn package jetty:run
Swagger UI will be available at http://localhost:8002
.
The relevant code is the index.html file which makes use of swagger-ui
bundle, downloaded and copied to resources in pom.xml:
(while sample code downloades a new version from master report at every build, you can use whatever mechanism to obtain the bundle)
Dig deeper
For further scenarios, documentation and samples, check out Integration and configuration.
OpenAPI 3.1 Support
OpenAPI 3.1 support details OAS 3.1 support in Swagger Core and Swagger Parser
Сегодня рассмотрим, что такое Swagger. А, чтобы понять, как работает этот инструмент тестирования и документирования REST API, разберем практический пример, собственноручно написав простенькое Python-приложение с FastAPI в интерактивной среде Google Colab и развернув его через ngrok – утилиту туннелирования локального сервера разработки в общедоступный URL.
Что такое Swagger и при чем здесь REST API
Обычно при тестировании RESTful API, а также при проектировании интеграции веб-приложений аналитик использует фреймворк Swagger от компании SmartBear, который позволяет интерактивно просматривать спецификацию и отправлять запросы. Он включает несколько компонентов, которые могут генерировать документацию на основе существующего кода на основе Java Annotation и создавать клиентов для нее. В визуальном веб-GUI Swagger можно просмотреть типы поддерживаемых HTTP-методов и описание схемы используемых данных, а также протестировать их. Также есть редактор, чтобы специфицировать REST API, т.е. получить документацию OpenAPI версии 3 в YAML или JSON форматах.
Чтобы аналитик или технический писатель мог получить доступ к веб-GUI Swagger тестируемого веб-приложения с REST API, разработчик должен развернуть его, используя SwaggerHub. Эта многопользовательская платформа позволяет определять REST API через спецификацию OpenAPI 3 и управлять ими. Далее рассмотрим, как это делается.
Основы архитектуры и интеграции информационных систем
Код курса
OAIS
Ближайшая дата курса
28 сентября, 2023
Длительность обучения
8 ак.часов
Стоимость обучения
15 000 руб.
Пишем свое RESTful Python-приложение с FastAPI
В качестве примера напишем небольшое RESTful Python-приложение с помощью фреймворка FastAPI в интерактивной среде Google Colab, которое будет поддерживать GET- и POST-запросы для сервиса клиентских заявок. Для простоты возьмем следующую структуру данных по клиентским заявкам, определив ее в JSON-формате:
{ "apps": [ { "course": "TTIS", "name": "Анна", "email": "anna@email.ru", "phone": "123456789" }, { "course": "MODP", "name": "Борис", "email": "boris@email.ru" } ] }
Конвертируем это JSON-сообщение в YAML-формат с помощью онлайн-конвертера https://www.json2yaml.com/, чтобы далее сгенерировать классы для нашего Python-приложения. Конвертация JSON-сообщения дает следующий YAML-документ:
--- apps: - course: TTIS name: Анна email: anna@email.ru phone: '123456789' - course: MODP name: Борис email: boris@email.ru
Используя полученный YAML, cгенерируем классы Python с помощью https://jsonformatter.org/yaml-to-python, чтобы не писать это самостоятельно:
from typing import Optional, List class App: course: str name: str email: str phone: Optional[int] def __init__(self, course: str, name: str, email: str, phone: Optional[int]) -> None: self.course = course self.name = name self.email = email self.phone = phone class Welcome6: apps: List[App] def __init__(self, apps: List[App]) -> None: self.apps = apps
Вставим полученные классы в код Python-скрипта с объявлением HTTP-методов для работы с веб-приложением. Чтобы упростить себе процесс разработки кода, поскольку я все-таки аналитик, а не разработчик, буду использовать веб-фреймворк FastAPI на основе Python 3.6+. Он позволяет быстро написать RESTful веб-приложение, используя стандартную аннотацию типов Python, а также, что наиболее важно в контексте Swagger, поддерживает автоматическую интерактивную документацию на основе открытых стандартов OpenAPI 3 и JSON Schema.
Дополним сгенерированные классы описанием функций, реализующих GET- и POST-запросы к объявленной структуре данных (@app.get() и @app.post() соответственно):
from typing import Union from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() from typing import Optional, List class App(BaseModel): id: int course: str name: str email: Optional[str] phone: Optional[str] class Welcome: apps: List[App] def __init__(self, apps: List[App]) -> None: self.apps = apps @app.get("/") def read_root(): return {"Привет, это мое REST API приложение"} @app.get("/apps/{app_id}") def read_app(app_id: int, q: Union[str, None] = None): return {"app_id": app_id, "q": q} @app.post("/apps/") def add_app(app: App): return {"app_id": app.id, "app_course": app.course, "client_name": app.name, "client_email": app.email, "client_phone": app.phone}
Реализация в Google Colab: веб-сервер unicorn с туннелем через ngrok
Поскольку обычно я не пишу код, разворачивать полноценную IDE на своем компьютере, нецелесообразно. Под мои задачи проверки гипотез и быстрого прототипирования вполне хватает возможностей интерактивной среды Google Colab. В этот раз я буду пользоваться ей снова, чтобы запустить вышеприведенный скрипт своего Python-приложения. Справедливости ради, стоит отметить, что этот скрипт лишь имитирует сервис работы с клиентскими заявками, поскольку мне было уже лениво разворачивать базу данных и прописывать инструкции подключения к ней. Впрочем, при желании все это тоже можно сделать в Goggle Colab, используя встроенную в Python легковесную реляционную СУБД SQLlite.
Возвращаясь к моему Pуthon-приложению, запущенному в Google Colab, чтобы получить доступ к нему и просмотреть сгенерированную фреймворком Fast API, документацию, нужно использовать какой-то веб-сервер, домен, хостинг и пр. Напомню, веб-сервер позволяет обращаться к серверному веб-приложению по протоколам HTTP и HTTPS, поддерживая HTTP-стандарты. Настройка всех этих ресурсов точно выходит за рамки компетенций аналитика и займет много времени. Поэтому я решила воспользоваться утилитой ngrok, которая позволяет поделиться локальным сервером разработки localhost, создав безопасный туннель с внешнего URL-адреса на локальный компьютер.
Утилита ngrok запускает на локальном компьютере небольшой клиентский процесс, который создает частный туннель подключения к облачной службе ngrok. Локальный сервер разработки localhost сопоставляется с поддоменом ngrok.io, к которому может получить доступ удаленный пользователь. При этом не нужно открывать порты, настраивать пересылки и выполнять прочие действия системного администратора.
Чтобы воспользоваться возможностями FastAPI и uvicorn – удаленного веб-сервера для Python, в блокноте Google Colab, сперва следует установить их:
# Install requirements !pip install fastapi==0.68.1 !pip install uvicorn==0.15.0
Затем следует установить пакет асинхронных вызовов:
!pip install nest-asyncio
Далее необходимо установить утилиту ngrok:
!pip install pyngrok
Потом идет ячейка с кодом моего Python-приложения:
from typing import Union from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() from typing import Optional, List class App(BaseModel): id: int course: str name: str email: Optional[str] phone: Optional[str] class Welcome: apps: List[App] def __init__(self, apps: List[App]) -> None: self.apps = apps @app.get("/") def read_root(): return {"Привет, это мое REST API приложение"} @app.get("/apps/{app_id}") def read_app(app_id: int, q: Union[str, None] = None): return {"app_id": app_id, "q": q} @app.post("/apps/") def add_app(app: App): return {"app_id": app.id, "app_course": app.course, "client_name": app.name, "client_email": app.email, "client_phone": app.phone}
Пробрасываем туннель для доступа к локальному серверу разработки из внешнего URL, сгенерированного утилитой ngrok. При этом сперва следует получить персональный токен разработчика на сайте этой платформы (https://dashboard.ngrok.com/signup), чтобы вставить его в код:
from pyngrok import ngrok auth_token = "персональный токен разработчика" #@param {type:"string"} # Since we can't access Colab notebooks IP directly we'll use # ngrok to create a public URL for the server via a tunnel # Authenticate ngrok # https://dashboard.ngrok.com/signup # Then go to the "Your Authtoken" tab in the sidebar and copy the API key import os os.system(f"ngrok authtoken {auth_token}")
Создаем сам туннель, чтобы получить публичный URL:
from pyngrok import ngrok # Create tunnel public_url = ngrok.connect(8000, port='8000', bind_tls=True)
И, наконец, запускаем свое Python-приложение с веб-сервером uvicorn:
import nest_asyncio # Allow for asyncio to work within the Jupyter notebook cell nest_asyncio.apply() import uvicorn # Run the FastAPI app using uvicorn print(public_url) uvicorn.run(app)
В заключение в отдельной ячейке пропишем закрытие проброшенного туннеля:
# Kill tunnel ngrok.disconnect(public_url=public_url)
Чтобы обойти ограничения AVG-антивируса моего компьютера, который не позволяет выполнять небезопасные подключения, я добавила в исключения два URL-адреса: https://cdn.ngrok.com/* и внешний URL,сгенерированный утилитой ngrok для туннелирования моего локального сервера разработки locallhost:
Поскольку FastAPI автоматически генерирует документацию, посмотрим на нее, обратившись к конечной точке /docs полученного URL. Видим привычный Swagger UI, где можно протестировать HTTP-методы своего REST API и модель данных.
Протестируем функцию добавления клиентской заявки через POST-запрос:
Чтобы добавить эту спецификацию API в SwaggerHub, получим ее в виде JSON-документа, обратившись к конечной точке /openapi.json полученного URL.
Скопировав содержимое этого JSON-документа, вставим его в SwaggerHub, где он автоматически конвертируется в YAML-формат. При этом вручную добавим раздел с объявлением серверов, чтобы указать URL-адрес uvicorn–сервера, туннелированного с помощью ngrok (выделено жирным):
openapi: 3.0.2 info: title: FastAPI version: 1.0.0 servers: # Added by API Auto Mocking Plugin - description: SwaggerHub API Auto Mocking url: https://virtserver.swaggerhub.com/VICHIGOVAANNA/FastAPI/1.0.0 - url: https://4b53-35-239-255-207.ngrok.io/ description: Production server paths: /: get: summary: Read Root operationId: read_root__get responses: '200': description: Successful Response content: application/json: schema: {} /apps/{app_id}: get: summary: Read App operationId: read_app_apps__app_id__get parameters: - required: true schema: title: App Id type: integer name: app_id in: path - required: false schema: title: Q type: string name: q in: query responses: '200': description: Successful Response content: application/json: schema: {} '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' /apps/: post: summary: Add App operationId: add_app_apps__post requestBody: content: application/json: schema: $ref: '#/components/schemas/App' required: true responses: '200': description: Successful Response content: application/json: schema: {} '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' components: schemas: App: title: App required: - id - course - name type: object properties: id: title: Id type: integer course: title: Course type: string name: title: Name type: string email: title: Email type: string phone: title: Phone type: string HTTPValidationError: title: HTTPValidationError type: object properties: detail: title: Detail type: array items: $ref: '#/components/schemas/ValidationError' ValidationError: title: ValidationError required: - loc - msg - type type: object properties: loc: title: Location type: array items: type: string msg: title: Message type: string type: title: Error Type type: string
Встроенный редактор SwaggerHub проверил валидность созданной YAML-спецификации и также предоставляет GUI для тестирования HTTP-методов, объявленных в приложении.
Надеюсь, что это небольшое руководство поможет аналитикам, которые только начинают знакомиться с REST API и Swagger, лучше понять их устройство и принципы работы. Как написать спецификацию OpenAPI вручную, а не получить из исходного кода, я показываю здесь. О том, что еще можно написать и запустить в Google Colab, читайте в новой статье про RabbitMQ.
Основы архитектуры и интеграции информационных систем
Код курса
OAIS
Ближайшая дата курса
28 сентября, 2023
Длительность обучения
8 ак.часов
Стоимость обучения
15 000 руб.
А подробнее познакомиться со всеми рассмотренными темами, а также другими основами архитектуры и интеграции информационных систем вам помогут курсы Школы прикладного бизнес-анализа в нашем лицензированном учебном центре обучения и повышения квалификации системных и бизнес-аналитиков в Москве:
- Основы архитектуры и интеграции информационных систем
Swagger — особый инструмент, автоматически документирующий интерфейс RESTful API вашего приложения.
Его достоинство заключается в том, что он позволяет не только изучить все эндпойнты приложения, но и сразу же протестировать их в деле, отправить любой запрос и получить ответ.
Для доступа к Swagger необходимо в опубликованном приложении перейти в Preview и нажать на наименование нужного плана публикации (Deploy Plan).
Открывшееся окно содержит список доступных endpoints и методов, связанных с этими endpoints. Некоторые запросы доступны только определенным группам авторизованных пользователей (проверяется в Middleware модуля Auth для каждого конкретного запроса в разделе Endpoints). Для запросов, требующих авторизации, нужно получить Bearer Token.
Для получения токена можно обратиться к соответствующему эндпоинту прямо в Swagger (раздел Auth, запрос POST /auth).
Далее, нужно нажать Try it out и ввести логин и пароль пользователя, для авторизации и получения токена.
Нажав Execute будет отправлен запрос. В ответе запроса, если он успешен, нужно найти поле token, которое и будет содержать токен авторизованного пользователя.
Второй способ получения токена авторизованного пользователя заключается в том, что в сгенерированном приложении, отправив запрос и получив ответ на него, можно найти токен в теле самого запроса. Для этого:
- Откройте инструмент разработчика в браузере (F12 в Google Chrome).
- Отправьте запрос на сервер (например, обновив данные таблицы). Запрос должен быть отправлен авторизованным пользователем, у которого есть доступ к endpoint.
- Открыть Network и выбрать соответствующий запрос.
- Во вкладке Headers найти раздел Request Headers и в заголовке Authorization получить необходимый токен.
Передать токен в Swagger можно, нажав на Authorize и вставив скопированный токен.
Далее можно приступать к тестированию запросов. Для этого необходимо выбрать нужную группу и метод в ней. Нажав Try it out и заполнив входные параметры запроса, нажмите Execute, чтобы выполнить запрос.
Самый ожидаемый ответ, в случае правильной обработки запроса сервером, имеет код 200 и показывает то, как должна выглядеть структура ответа.
Остальные коды показаны скорее для некоторого ориентира на стандарт и применимы для автосгенерированных запросов.
-
401 — запрос не был выполнен успешно, так как необходимый токен авторизации отсутствует или не верен.
-
404 — запрос был обработан успешно, но искомый ресурс не найден.
-
422 — были переданы неправильные параметры на вход запроса.
-
500 — ошибка обработки запроса сервером.
Получение кастомных ошибок
Для кастомных БП и связанных с ними запросов возможно создавать свои коды ошибок с описанием с помощью блока Raise Error в редакторе БП. Пример такого БП ниже:
В этом случае, при неудачной попытке запроса к эндпоинту, связанному с БП выше, сервер выдаст ошибку 418, содержащую текст ошибки при выполнении блока DB: Create Candidate. Код ошибки в данном примере может любой, который задаст пользователь.
Примечание: HTTP код ошибки 418 I’m a teapot сообщает о том, что сервер не может приготовить кофе, потому что он чайник. Эта ошибка ссылается на Hyper Text Coffee Pot Control Protocol (гипертекстовый протокол кофейников) который был первоапрельской шуткой в 1998 году.