Русский
Русский
English
Статистика
Реклама

Swagger

Создание самодокументирующегося сервера на Node.JS

20.01.2021 14:20:49 | Автор: admin

Условия:

  • валидация через Joi

  • использование Typescript

  • Express сервер

  • SWAGGER на /api-docs

Задача: DRY

Решение:

Для начала необходимо решить что первично: схема Joi, Swagger или TypeScript интерфейс. Эмпирическим путём установлено что первичной стоит сделать Joi.

1. Установка всех модулей на Express

npm install --save swagger-ui-express

Добавить строки в app.ts (index.ts):

import swaggerUI = require('swagger-ui-express')import swDocument from './openapi'...app.use('/api-docs',swaggerUI.serve,swaggerUI.setup(swDocument))

2. Создать ./openapi.ts

В этом файле содержится основные сведения о сервере. Создать его (как и все схемы, приведённые ниже) можно с помощью SWAGGER-утилиты. Важно выбрать при этом протокол openapi v3.0.0

Пример содержимого:

import {swLoginRoute} from './routes/login'const swagger = {  openapi: '3.0.0',  info: {    title: 'Express API for Dangle',    version: '1.0.0',    description: 'The REST API for Dangle Panel service'  },  servers: [    {      url: 'http://localhost:3001',      description: 'Development server'    }  ],  paths: {    ...swLoginRoute  },}export default swagger

Пути забираются из роутеров через инклуды.

3. Написать спецификацию роутера

В каждом роутере добавить openapi-описание

Пример ./routes/login/index.ts:

import {swGetUserInfo} from './get-user-info'import {swUpdateInfo} from './update-info'export const swLoginRoute = {  "/login": {    "get": {      ...swGetUserInfo    },    "patch": {      ...swUpdateInfo    }  }}

Выше описан путь /login, поддеживающий два метода: get и patch. Спецификации методов берутся инлудами из файлов get-user-into.ts и update-info.ts. Эти же файлы у меня содержат сами роуты.

4. Написать спецификацию роута и валидацию данных

Спецификация роута будет создаваться автоматически, на основе Joi-схемы.

Для начала сделаем инклуд будущей схемы в нашем роуте.

Примечание: совершенно не важно как вы располагаете ваши файлы, если соответственно модифицируете инклуды.

Строки из файла update-info.ts, в котором расположен мой роут (код код его самого нам не важен):

import schema, {joiSchema} from './update-info.spec/schema'export const swUpdateInfo = {  "summary": "update the user info",  "tags": [    "login"  ],  "parameters": [    {      "name": "key",      "in": "header",      "schema": {        "type": "string"      },      "required": true    }  ],  "requestBody": {    "content": {      "application/json": {        "schema": {          ...schema        }      }    }  },  "responses": {    "200": {      "description": "Done"    },    "default": {      "description": "Error message"    }  }}// ...далее идёт код роута

Этот JSON-объект можно сгенерить той же Swagger-утилитой, чтобы не мучать себя. Обратите внимание на следующую строку:

"schema": {  ...schema}

Это обеспечивает динамическое подключение нашей схемы.

Теперь можно добавить Joi-валидацию в роуте:

await joiSchema.validateAsync(req.body)

4. Пишем Joi-схему

Установка Joi:

npm install --save joi joi-to-swagger

Пример содержимого файла:

const joi = require('joi')const j2s = require('joi-to-swagger')// Joiexport const joiSchema = joi.object().keys({  mode:    joi.string().required(),  email:   joi.string().email()})// end of Joiconst schema = j2s(joiSchema).swaggerexport default schema

Данный файл осуществляет экспорт Joi-объекта и его swagger-схемы.

Чтож, на данном этапе у нас уже есть самодокументирующийся SWAGGER-сервер и валидация данных. Осталось настроить автоматическую генерацию TypeScript-интерфейсов

5. Генерация интерфейсов TypeScript

npm install --save-dev gulp @babel/register @babel/plugin-proposal-class-properties @babel/preset-env @babel/preset-typescript

Задачи на себя возьмёт Gulp. Это самая чувствительная часть системы, которую нужно настроить вручную под структуры вашего проекта. Вот как выглядит gulpfile.ts у меня:

const gulp = require('gulp')const through = require('through2')import { compile } from 'json-schema-to-typescript'const fs = require('fs')const endName = "schema.ts"const routes = `./routes/**/*.spec/*${endName}`function path(str: string) : string{   let base = str   if(base.lastIndexOf(endName) != -1)     base = base.substring(0, base.lastIndexOf(endName))   return base}gulp.task('schema', () => {  return gulp.src(routes)    .pipe(through.obj((chunk, enc, cb) => {      const filename = chunk.path      import(filename).then(schema => { // dynamic import        console.log('Converting', filename)        compile(schema.default, `IDTO`)          .then(ts => {            //console.log(path(filename).concat('interface.ts'), ts)            fs.writeFileSync(path(filename).concat('interface.ts'), ts)          })        })      cb(null, chunk)    }))})// watch serviceconst { watch, series } = require('gulp')exports.default = function() {  watch(routes, series('schema'))}

Скрипт обходит все подкаталоги с названием *.spec внутри каталога с роутера. Там он ищет файлы с именами *schema.ts и создаёт рядом файлы c именами *interface.ts

Заключение

Разумеется, эти большие и сложные JSON-объекты с openAPI-спецификацией пугают, но надо понимать, что они не пишутся вручную, а геренятся SWAGGER-утилитой.

Из-за неопытности первичная настройка механизма может занять какое-то время, но это приведет к экономии сотен часов, которые могли бы быть потрачены на рутину!

Подробнее..
Категории: Javascript , Node.js , Api , Express , Swagger , Openapi , Joi

Справочный центр Selectel интерфейс, техническая реализация и возможности

19.08.2020 12:13:25 | Автор: admin

Каждой предоставляемой услугой Selectel можно управлять в личном кабинете панели управления. Многими нашими продуктами также возможно управлять через запросы к API. Инструкции по работе с продуктами и документация API доступны в едином справочном центре.

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

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

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

Около года назад мы обратили внимание на то, что существующая онлайн-справка не отвечает требованиям наших клиентов:

  • меню навигации разрасталось, как плодовое дерево, не знающее заботы садовника;
  • поиск перестал быть удобным;
  • визуальное оформление постепенно устаревало.

Ранее документации к API наших продуктов не было в открытом доступе часть была размещена в панели управления my.selectel.ru, что-то выдавалось по запросу, а что-то было и вовсе описано в текстовом формате, поддерживать который в актуальном состоянии было весьма проблематично. Теперь знания по автоматизации IT-инфраструктуры и взаимодействию с бэкендом сервисов Selectel на вкладке Документация API.

Небольшой экскурс в прошлое


Еще десять-пятнадцать лет назад мало кто из компаний, производящих программное обеспечение в России, мог похвастаться наличием онлайн-справки. Руководства по эксплуатации писали в формате Word с применением вечно съезжающих стилей, пытаясь применить ГОСТы, созданные в 70-х годах 20 века к современным и постоянно меняющимся требованиям.

В среде технических писателей всё ещё популярен мем:


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

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

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

  1. Пользователь заказал услугу.
  2. Столкнулся с неочевидными моментами при настройке.
  3. Хотел обратиться в техническую поддержку.
  4. Зашел в базу знаний и нашел нужную информацию самостоятельно.
  5. Не обратился в техническую поддержку.
  6. Профит!)

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


Большинство компаний, производящих ПО, на определённом этапе разрабатывают API как для внутреннего использования, так и для клиентов. Компании делятся набором вводных параметров, выпуская публичные API для того, чтобы дать клиентам возможность объединять свои сервисы и предлагаемые компанией продукты.

Многие пользовательские задачи можно автоматизировать, в том числе используя готовые куски кода как конструктор для создания максимально удобной в использовании инфраструктуры. Но куски кода без объяснений, что это такое, что оно делает и как этим пользоваться малополезны не будешь ведь перебором выяснять, какие есть методы и какие данные они ожидают на вход.
Что нужно сделать, чтобы публичным API было приятно пользоваться?
Приложить документацию!
В качестве документации к API обычно дают минимальный пример взаимодействия, в котором описаны методы (также их называют эндпоинтами) и приведены ответы от сервиса. Например, с помощью API Kubernetes можно автоматизировать работу с кластерами и нодами в Managed Kubernetes.

Какие задачи стояли перед нами


Наше визуальное решение перестало позволять легко находить нужные статьи, и существующее меню превратилось в нечитаемую простыню. Нечитабельная документация также плоха, как неактуальные тексты. Люди привыкли читать хорошо сверстанные материалы. Блоги, соцсети, медиа весь контент подается не просто красивым, но и удобным для чтения, приятным для глаза. В настоящее время нельзя игнорировать UX и UI требования, тем более для такого огромного массива текстов.

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

Часто возникающие проблемы с документацией


Вообще отсутствует или никто не знает о ее существовании
Инструкция, которую нельзя найти, ничем не лучше инструкции, которой нет.

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

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

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

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

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

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


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

Как это работает теперь


Техническая реализация


Первоначально база знаний была построена на основе объединения Confluence и генератора статических страниц.

Мы отказались от Confluence и перешли к принципу Documentation-as-Code. Сейчас все исходные тексты для базы знаний верстаются в формате Markdown и хранятся в системе хранения версий Git. Из хранилища с помощью генератора статических сайтов Hugo собирается сайт базы знаний.

Сайт генерируется из файлов, содержащихся в master-ветке Git-репозитория. Обновить данные можно только через pull-request, что позволяет проверять все новые разделы документации перед тем, как они будут опубликованы в общем доступе. Такая система также облегчает подход к внесению правок сотрудники компании всегда могут создать ветку и добавить в нее все нужные изменения.

Подробнее о Documentation as Code


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

  • языки разметки текста;
  • системы контроля версий;
  • code review.

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


Работа со swagger-файлами


Разработчики продуктов Selectel формируют API, и выгружают комментарии к коду в swagger-формате это текстовый файл в в формате *.yaml, с которым можно работать как с кодом. При обновлении API обновляется и swagger-файл, что позволяет упростить актуализацию документации.

Для документации API используется следующее техническое решение:

  • git-репозиторий с файлами swagger-спецификаций в формате *.yaml, сгруппированными по продуктам;
  • git-репозиторий с переводами swagger-спецификаций в формате *.json;
  • git-репозиторий с файлами в формате *.md;
  • файлы в формате *.md содержат текстовые описания и обернутые в специальные теги описания эндпоинтов, которые подтягиваются из git-репозитория с файлами в формате *.json;
  • генератор статических сайтов Hugo.

Кроме структуры репозитория были разработаны правила работы с ним:

  • подготовлен стайлгайд чек-лист для swagger-файлов, с которым сверяются разработчики при обновлении спецификаций API;
  • ветвь master git-репозитория с файлами в формате *.md отражает состояние актуальных спецификаций и де-факто находится на продакшене;
  • Pull-Request в master-ветку осуществляется при выпуске обновлений в боевую эксплуатацию.

Визуальная составляющая


Первым делом требовалось пересмотреть навигацию и создать правила, по которым бы формировались разделы базы знаний. Совместно с UX-проектировщиками мы подготовили информационную архитектуру. Структура должна быть максимально прозрачной, чтобы читатель не задумывался: А где можно найти эту статью?. Если обобщить, то есть два подхода:

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

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

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

Каждая компания делает документацию API в соответствии с собственным видением стиля, структуры и чувства прекрасного. Если открыть 4-5 публичных API разных компаний, то, несомненно, мы сможем уловить некие общности: структура, объемные примеры кода и подсветка синтаксиса, длинные страницы. Но тем не менее все примеры будут обладать особенностями. Например, локализация описаний эндпоинтов встречается редко, притом что как раз структура описания эндпоинтов чаще всего выглядит одинаково.

Несколько рекомендаций к структурированию документации API:

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

Отдельное описание методов
При одновременном использовании методов GET/POST в одной строчке должен быть описан только один метод.

То есть первой строчкой в этом случае идет описание GET, второй строчкой описание POST, благодаря чему можно избежать путаницы.


Автоматизация внесения изменений
Упростить внесение изменений без ручного переформатирования каждого раздела в нашем случае мы переводим только текст swagger-файла, не трогая разметку благодаря автоматизации этого процесса, который настроили наши devops-специалисты.

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

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

Так как из коробки была доступна подсветка кода только для темного фона, frontend-разработчик дорабатывала это решение с помощью highlight.js до нашего корпоративного стиля.

Вместо заключения


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


(Льюис Кэрролл Алиса в Зазеркалье)
То, что мы уже сделали, лишь очередной шаг на пути совершенствования базы знаний. Постепенно мы будем добавлять новые инструкции и документацию к API других наших сервисов.
Подробнее..

Создаем приложение на Node.JS, Express и Typescript с Jest, Swagger, log4js и Routing-controllers

11.01.2021 10:24:00 | Автор: admin
Это пошаговая инструкция создания приложение на Node.JS, с использованием typescript и express. Новое приложение создается не часто, отсюда забываются шаги по его созданию. И я решил написать некую шпаргалку, в помощь самому себе и другим разработчикам. Помимо шагов, я так же снял небольшие видео ролики для наглядности. Существуют уже готовые фреймворки для Node.JS, которые уже содержат в себе все необходимые пакеты и можно работать с ними, но это уже другой путь. Идея была в том, чтобы не зависить целиком от какого-то фреймворка и в случае необходимости менять одни пакеты на другие.

Итак по шагам:
  1. Простое Web приложение youtu.be/7MIIcFDeSg4
    Ставим в определенном порядке пакеты и Node.JS, а так же прописываем настройки.
    1) node.js download, 2) Create directory for your project, 3) npm init, 4) in package.json "main": "dist/index.js",  "scripts": {    "build": "tsc",    "start": "node ."  }5) npm install --save-dev typescript, 6) in tsconfig.json   {  "compilerOptions": {    "esModuleInterop": true,    "outDir": "dist",    "baseUrl": "."  }}8) npm install express, 9) npm install @types/express, 10) create src folder, 11) create src/index.ts with code below:import express from 'express'const app = express();const port = 5000;app.get('/', (request, response) => {  response.send('Hello world!');});app.listen(port, () => console.log(`Running on port ${port}`));13) npm run build, 14) npm run start, 15) localhost:5000
    

  2. Отладка и инициализация в Node.js youtu.be/hfST0e1ITGw
    Настраиваем режим отладки и создаем .env файл для установки входных значений.
    1) in tsconfig.json add: "sourceMap": true2) int package.json add: "prestart": "npm run build",3) In IntelliJ IDEA in Run/Debug Configurations choose: "npm" and add script4) npm i ts-node-dev --save-dev5) int package.json add: "server:watch": "ts-node-dev --respawn --transpile-only src/index.ts"6) add IntelliJ IDEA npm for "server:watch" script7) npm install dotenv8) in index.ts add: dotenv.config();9) create .env file in root dir of your project and add text below in .env file:PORT = 5000const port = process.env.PORT;
    

  3. Добавление log4js и eslint к приложению на Node.JS youtu.be/qcSpd6N7ZJ8
    1) npm install log4js2) in index.ts file:    import log4js from 'log4js';    ...    const logger = log4js.getLogger();    logger.level = process.env.LOG_LEVEL;    ...4) in .env file: LOG_LEVEL=error5) in index.ts file:    ...    logger.info('log4js log info');    logger.debug('log4js log debug');    logger.error('log4js log error');    ...6) npm install eslint --save-dev7) eslint --init8) "prebuild": "npm run lint"9) "lint:fix": "eslint --cache --ext .ts . --fix",10) "lint": "eslint --cache --ext .ts .",    !!! --cache (only changed), .11) IntelliJ IDEA -- file -- setting -- eslint -- automatic12) "rules": {        "semi": ["error", "always"]    }
    

  4. Routing controllers для Node.js youtu.be/_7z5Zubsdps
    Используем routing-controllers для более удобной работы.
    1) npm install routing-controllers2) npm install reflect-metadata3) npm install express body-parser multer4) npm install class-transformer class-validator5) tsconfig.json   "compilerOptions": {      ...      "emitDecoratorMetadata": true,      "experimentalDecorators": true      ...   }6) in index.ts// const app = express();// logger.info('log4js log info');// logger.debug('log4js log debug');// logger.error('log4js log error');// app.get('/', (request, response) => {//   response.send('Hello world2!');// });7) in index.ts   import { createExpressServer } from 'routing-controllers';   import { UserController } from './UserController';   const app = createExpressServer({     controllers: [UserController], // we specify controllers we want to use});8) controller/user-controller.ts   import { Controller, Get, Param } from 'routing-controllers';   import 'reflect-metadata';   @Controller()   export class UserController {     @Get('/users/:id')     getOne (@Param('id') id: number) {       return 'This action returns user #' + id;     }   }9) http://localhost:3001/users/1
    

  5. Node.JS middleware, interceptor, http context youtu.be/iWUMUa7gTTQ
    1) middleware -- middleware.ts2) middleware.tsexport function loggingBefore (request: any, response: any, next?: (err?: any) => any): any {  console.log('do something Before...');  next();}export function loggingAfter (request: any, response: any, next?: (err?: any) => any): any {  console.log('do something After...');  next();}3) user-controller.ts in class@UseBefore(loggingBefore)@UseAfter(loggingAfter)console.log('do something in GET function...');4) user-controller.ts in function @UseBefore(loggingBefore) @UseAfter(loggingAfter)5) user-controller.ts in function @UseInterceptor(function (action: Action, content: any) {    console.log('change response...');    return content;  })6) npm install express-http-context7) index.ts  const app: Express = express();        app.use(bodyParser.json()); app.use(httpContext.middleware); useExpressServer(app, {   controllers: [UserController] }); app.use((req, res, next) => {   httpContext.ns.bindEmitter(req);   httpContext.ns.bindEmitter(res); });8) middleware.ts loggingBefore    import httpContext from 'express-http-context';        console.log('set traceId = 123');    httpContext.set('traceId', 123);9) middleware.ts loggingAfter    console.log(`tracedId = ${httpContext.get('traceId')}`);
    

  6. Node.JS добавляем post запрос, валидация входных данных, глобальный обработчик ошибок youtu.be/onBVkkLEuw4
    1) in user-controller.ts add:  ...  @Post('/users/:id')  @OnUndefined(204)  postOne (@Param('id') id: number, @Body() info: any) {    console.log(JSON.stringify(info));  }  ...2) in postman http://localhost:3001/users/1 { "country":"Russia", "city":"SPb" }3) model -- info.ts4) import { IsDefined } from 'class-validator';export class Info {  @IsDefined()  country: string;  @IsDefined()  city: string;}8) postOne (@Param('id') id: number, @Body() info: Info) {9) middleware -- global-error-handler.ts10) import { ExpressErrorMiddlewareInterface, Middleware } from 'routing-controllers';@Middleware({ type: 'after' })export class GlobalErrorHandler implements ExpressErrorMiddlewareInterface {  error (error: any, request: any, response: any, next: () => any) {    response.send({ ERROR: error });    next();  }}11) useExpressServer(app, {  controllers: [UserController], // we specify controllers we want to use  middlewares: [GlobalErrorHandler],  defaultErrorHandler: false});
    

  7. Swagger документация в Node.JS приложении youtu.be/-uoIasCbsq8
    1) npm install swagger-ui-express2) tsconfig.json -- "resolveJsonModule": true3) src -- swagger -- openapi.json4) index.tsimport swaggerUi from 'swagger-ui-express';import * as swaggerDocument from '../src/swagger/openapi.json';...app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));5) change port to 3000in .env file set PORT=30006) npm install cors7) npm install @types/cors8) in index.ts import cors from 'cors';...app.use(cors() as RequestHandler);...9) Swagger Editor (example for test project)openapiopenapi: 3.0.1info:  title: test API  version: v1servers:  - url: 'http://localhost:3000'tags:  - name: API functions    description: >-      API functions of our application      paths:  /users/{id}:    get:     summary: returns simple answer from get     tags:       - API functions     parameters:       - name: id         in: path         required: true         description: simple parameter         schema:           type : string           example: '1'     description: parameter id just for test     responses:      '200': #status code       description: OK       content:            document:              schema:                type: string                example: some text    post:     summary: returns simple answer from post     tags:       - API functions     requestBody:        required: true        content:          application/json:            schema:               $ref: '#/components/schemas/Info'                      example:              country: Russia              city: Spb     parameters:       - name: id         in: path         required: true         description: simple parameter         schema:           type : string           example: '1'     description: parameter id just for test     responses:      '204': #status code       description: OKcomponents:  schemas:    Info:      type: object      properties:        country:          type: string        city:            type: string
    

  8. Добавляем Unit тесты на Jest в приложение на Node.JS youtu.be/rCIRpTMVEMM
    0) in global-error-handler.tsresponse.status(error.statusCode || error.httpCode).json(error);    next();1) npm install --save-dev jest2) npm i -D ts-jest @types/jest3) npm i -D ts-jest4) package.json -- {...scripts {..."test:unit": "jest --config=jest.config.js",},...}5) create jest.config.js with code below:process.env.NODE_ENV = 'UNITTEST';module.exports = {    clearMocks: true,    collectCoverage: true,    collectCoverageFrom: [        './src/**/*.ts'    ],    coverageDirectory: '<'rootDir>/test/coverage',    testEnvironment: 'node',    testMatch: ['**/*.test.ts'],    preset: 'ts-jest'};6) .eslintignore*.jsnode_modulesdistcoverage}7) .eslintrc.json{..."env": {   "jest": true} ...} 8) test -- controller -- user-controller.test.tsdescribe('UserController', () => {  afterEach(() => {    jest.restoreAllMocks();  });  it('postOne', () => {    const userController = new UserController();    const testBody = {      city: 'SPb'    };    const res = userController.postOne(1, testBody as Info);    expect(res).toBeUndefined();  });}9) in IDEAadd script - test:unitset in environment - NODE_ENV=UNITTEST10) Simple variant of jest.config.js for IDEA:process.env.NODE_ENV = 'UNITTEST';module.exports = {  clearMocks: true,  collectCoverage: false,  testEnvironment: 'node',  testMatch: ['**/*.test.ts'],  preset: 'ts-jest'};11) npm i -D supertest @types/supertest12) in user-controller.test.ts...let server;...beforeAll(async () => {    server = express();    server.use(bodyParser.json());    useExpressServer(server, {      controllers: [UserController], // we specify controllers we want to use      middlewares: [GlobalErrorHandler],      defaultErrorHandler: false    });  });...it('postOne with validations', done => {    request(server)      .post('/users/1')      .send({        country: 'Russia',        city: 'SPb'      } as Info)      .expect(204)      .end((err, res) => {        if (err) throw new Error(JSON.stringify(res.body));        done();      });  });
    

  9. Использование config для Node.JS, а так же другие полезные пакеты. youtu.be/8ZCHUN-JTck
    Пакет config позволяет устанавливать значения констант при инициализации в зависимости от значения NODE_ENV.
    1) npm install config2) npm install @types/config3) config4) default.yaml PORT: 3000    DEV.yaml PORT: 3001    LOCAL.yaml PORT: 3002 5) index.ts   // const port = process.env.PORT;      const port = config.get('PORT');6) IDEA server:watch -- Environment    NODE_ENV=DEV    NODE_ENV=LOCAL-- packages:husky - коммиты в гитsemantic-release - формат коммитов и контроль версийpretty-quick - запускает prettier на измененных файлахprettier - формат кодаeslint-config-prettier - разрешает конфликты между eslint и prettiereslint-plugin-prettier - запускает prettier как правила eslintmock-socket - мок для вебсокетаjest-websocket-mock - тестирование вебсокетаjest-sonar-reporter - конвертр из формата jest в формат sonarjest-mock-extended - мок объектов и интерфейсовws - вебсокетtypescript-string-operations - String.formatlodash - библиотека дополнительных функций для jshttp-status-codes - константы для HTTP статусовmoment - библиотека работы со временем в jsncp - копирование файловjs-yaml - загрузка yaml файловmongodb - функции для работы с Mongomigrate-mongo - миграция для Mongolog-timestamp - запись даты в логaxios - HTTP клиентapplicationinsights - интеграция с Azure Application Insights
    

Подробнее..

SwaggerOpenAPI Specification как основа для ваших приёмочных тестов

02.11.2020 12:06:03 | Автор: admin

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


Я занимаюсь автоматизацией тестирования в Яндексе с 2013 года. Из них более четырёх лет автоматизирую тестирование REST API-сервисов. На Heisenbug я рассказал об использовании OpenAPI-спецификации как основы для приёмочных тестов, а также о том, как легко поддерживать автотесты на огромное количество REST API-сервисов и добавлять автотесты на новые проекты.



Под катом видеозапись и расшифровка моего доклада. Примеры из доклада есть на GitHub.



Как всё устроено


Яндекс.Вертикали это три больших сервиса: Яндекс.Недвижимость, Яндекс.Работа и Auto.ru. Они имеют микросервисную архитектуру. Большинство бэкендов это REST API-сервисы с разной кодовой базой, которые активно развиваются. К тому же у каждого REST API может быть несколько версий, которые также необходимо тестировать, чтобы старые клиенты не ломались при глобальных изменениях.


Команда


Наша команда это четыре-пять человек. Это люди, которые занимаются инструментами для автоматизации, инфраструктурой, пишут и встраивают автотесты в процесс разработки. Я занимаюсь также мобильным направлением: инфраструктурой для автоматизации тестирования под iOS и Android. В автотестах на клиенты мы активно используем моки, поэтому мы не можем позволить себе тестировать наш REST API через клиент.


Итого:


  • Несколько десятков бэкендов с разной кодовой базой, которые активно развиваются.
  • Маленькая команда автоматизации, состоящая всего лишь из четырёх-пяти человек.
  • Активное использование моков в тестировании.

С каким опытом мы подошли к нашей задаче


Три года назад картина у нас была следующая. У нас были автоматизаторы тестирования, которые имели достаточно стандартный подход и писали автотесты на Apache HTTP-клиенте. У нас были ручные тестировщики, которые не могли писать достаточно сложный код, поэтому использовали инструменты в виде Postman и писали автотесты, используя JavaScript. И у нас были разработчики, которые писали в основном юнит-тесты, интеграционные тесты. А некоторые вообще не понимали, зачем нужны автотесты, так как считали, что ничего не сломается.


Получалось, что все члены команды имели разные подходы к автоматизации тестирования. К тому же есть ещё одна важная проблема наши REST API активно развиваются. Это означает, что при новых релизах в наших сервисах нам нужно править тестовый клиент. По факту у нас происходит гонка нашего тестового клиента и REST API. Наши клиенты устаревают очень быстро. Скажем так, у нас есть несколько десятков REST API и несколько десятков тестовых клиентов, которые нужно поддерживать. И это адский труд, который отнимает огромное количество времени и вообще не имеет никакого отношения к автоматизации тестирования.


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


В связи с этим мы сформировали определённые требования к автотестам:


  • Мы решили, что автотесты должны писать все члены команды. И ручные тестировщики, и автоматизаторы тестирования, и разработка. И понимать автотесты должны все члены команды.
  • Автотесты должны легко встраиваться в новые проекты. Соответственно, нужна фабрика автотестов.
  • Автотесты нужно как можно реже править, желательно только когда добавляется новая бизнес-логика.

Об эволюции автотестов на REST API


Необходимо понимать, с каким опытом мы пришли к этой задаче. Давайте поговорим об эволюции автотестов, которую мы прошли. Изначально мы писали автотесты на Apache HTTP client. Поняв, что дублируем много кода и он очень громоздкий, мы написали свою обвязку над HTTP client-ом. Это немного сокращало наши труды. Когда появились специализированные инструменты для автоматизации и появился REST Assured, мы начали его использовать. Потом мы осознали, что его тоже неудобно использовать, и написали свою обвязку над REST Assured. Всё это была эра клиента.


В какой-то момент мы поняли, что очень часто дублируем bean-ы для реквестов, для респонсов и решили их генерировать из JSON Schema. Это оказалось очень удобно: у нас переиспользуется код. Код упростился, и нам это очень понравилось. Стало ясно, что можно генерировать не только bean-ы, но и из них генерировать assertion-ы и получать типизированные assertion-ы для этих bean-ов.


Позже мы поняли, что можно генерировать не только bean-ны, assertion-ы, но ещё и тестовый клиент. Мы стали генерировать клиент на основе RAML-спецификации. Это тоже экономило много времени и делало клиент единообразным. У нас уменьшалось время внедрения людей в новый проект автотестов. Затем мы решили не генерировать bean-ы, а сразу брать их напрямую из кода и генерировать их в проекте автотестов. Мы назвали это эрой кодогенерации.



Ещё очень важный момент. У нас во всех проектах есть спецификации. В основном это спецификации двух версий это OpenAPI-спецификации v1.0 и OpenAPI-спецификации v2.0. В какой-то момент пришёл менеджер и сказал, что мы больше не будем релизить новые REST API-сервисы без спецификаций.
Зачем мы вообще используем спецификацию? Всё благодаря этой замечательной странице Swagger UI.



Из неё нам понятно, какой перед нами API, понятны все операции, понятно, как API используется и что вернётся. Это экономит огромное количество времени разработке для коммуникации с фронтендерами, разработчиками мобильных приложений, с ребятами, которые занимаются клиентами. Более того, через эту страницу можно делать запросы и получать ответы. Это оценили наши тестировщики, которые могут, не используя Curl, тестировать релиз. Исходя из этого мы решили, что будем строить наши автотесты на основе кодогенерации, и в качестве основы мы возьмём Swagger/OAS.


Мы решили строить такой процесс: у нас будет REST API, из него мы будем получать OpenAPI-спецификацию, а затем из OpenAPI-спецификации тестовый клиент, с помощью которого мы и будем писать автотесты.


Что такое OpenAPI-спецификация


OpenAPI-спецификация это opensource-проект, описывающий спецификацию и поддерживаемый линукс-сообществом (Linux Foundation Collaborative Project). Это популярный проект, у него 16000 звёздочек на GitHub.


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


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



Наш Swagger UI и строится на основе этого файла спецификации swagger.json.



Как получить OpenAPI-спецификацию


Самый простой способ написать её в текстовом файлике. Это долго и не очень удобно.


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



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


Есть третий способ для получения спецификации: через Swagger-annotation в коде. Допустим, у вас есть какой-то API-ресурс, вы описываете для него Swagger-annotation. Annotation processor обработает аннотации вашего сервиса и вернёт спецификацию. Для каждого релиза вашего REST API получаем всегда актуальную OpenAPI-спецификацию. Если вы что-то удаляете, у вас автоматически это удаляется из OpenAPI-спецификации. И этот процесс постоянный.



Генерация клиента


Давайте разберёмся теперь с генерацией клиента. В opensource есть два больших, достаточно популярных проекта. Это Swagger Codegen, он на данный момент поддерживается компанией SmartBear. У него 11000 звёздочек на GitHub. И OpenAPI Generator, тоже opensource-проект, но он поддерживается комьюнити. В основном про него я и буду говорить.


По факту OpenAPI Generator является форком Swagger Codegen. Он отбренчевался от этого проекта в 2018 году.


Это произошло в связи с независимым развитием Swagger Codegen 3.X и Swagger Codegen 2.X. Из-за этого нарушилась обратная совместимость. Очень много клиентов исчезли и не были поддержаны. И ещё одна причина это нестабильность релизного цикла. Релизы в Swagger Codegen были довольно редкие, тесты часто падали и комьюнити это не устраивало.


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



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


Вы получаете готовый проект. Там есть и клиент, и тест, даже скрипт, который пушит код на GitHub. По факту этот проект уже можно использовать. Но как только вы его начнёте использовать, то поймёте, что что-то идёт не так. Ниже реальный пример, где я использовал генерацию кода.



На самом деле мы пытаемся генерировать клиент из спецификации, которая на это не рассчитана. Данная спецификация использовалась только для Swagger UI, а мы хотим получить клиент. И мы можем получить что-то невразумительное. Вместо методов вашего тестового клиента у вас будут route1, route2, route16.


Проблемы, с которыми мы столкнулись при генерации


Также вы получите другие различные проблемы. Например, опечатки, потому что Swagger Annotation пишется руками разработчиков. Опечатки можно поправить это не проблема. Могут быть различные проблемы с повторением моделей. Это достаточно легко решается, если к модели добавить имя пакета. И ещё одна проблема неполнота спецификаций. Скоро вы обнаружите, что в вашем API есть внутренние операции, о которых вы не знали, но которые тоже надо тестировать. Самое приятное, что всё это легко исправляется.


Но есть тонкий момент этот клиент и весь проект мы получаем один раз во время генерации. То есть проблема осталась: напомню, что при любом изменении REST API нам придётся снова поддерживать тестовые клиенты. Тогда мы решили, что будем генерировать клиент до запуска тестов и будем делать это с помощью плагина. OpenAPI Generator поддерживает множество плагинов. Например, это maven-plugin, gradle-plugin, sbt-plugin и bazel-plugin. В качестве примера я возьму maven-plugin.


Мы добавляем в наш pom.xml maven-plugin с определёнными настройками, указываем путь к нашей спецификации, папку для результата, язык генерации, dateLibrary, флаги валидировать вашу спецификацию или нет во время генерации, генерировать ли ваш клиент, если спецификация не менялась.



После компиляции у нас получается готовый клиент в target. Его можно использовать. По факту мы получили постоянный процесс: прямо из REST API с помощью Swagger-annotation мы получаем OpenAPI-спецификацию; из OpenAPI-спецификации мы получаем наш тестовый клиент.


Что делать, если у REST API несколько версий?


Например, есть версия v1.x, v2.x и vN.x. На самом деле всё то же самое. Мы в наш плагин добавляем второй . То есть у нас получается два , для версии v1 и для версии v2.


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


Как добавляли клиент


Вернёмся в 2018 год. Когда мы только всё начинали, мы рассматривали множество клиентов в Swagger Codegen, написанных под разные языки, но ни один нас не устроил. Все эти клиенты очень жёстко привязаны к документации. В них мало точек расширения, и они не рассчитаны на то, что наша спецификация будет меняться. Мы решили, что напишем свой API-клиент, который будет обладать всеми необходимыми для нас возможностями.


В качестве библиотеки мы выбрали REST Assured. Она имеет fluent interface, эта библиотека предназначена для тестирования. В ней есть механизм Request specification и Response specification.


Сама генерация клиента в OpenAPI Generator основана на mustache template (Logic-less Mustache engine). Это круто, потому что генерация не зависит от языка программирования. Вы можете использовать mustache-шаблоны как для C#, так и для С++, так и для любого языка и получить клиент. Ещё один плюс эти клиенты легко добавлять. Вам надо только добавить набор mustache-шаблонов, и у вас готовый клиент. И третья очень крутая фича эти клиенты очень легко кастомизировать. Достаточно добавить свои шаблоны, которые просто будут использоваться при генерации.


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


Аналогично можно получить те же переменные, но уже для моделей.


Итак, мы получили все переменные, написали все шаблоны и соответственно сделали pull request в Swagger Codegen. И этот pull request приняли. Теперь у нас есть собственный клиент.


Давайте рассмотрим его подробнее. Возьмём в качестве примера простейшую операцию GET /store/Inventory из Story API и попробуем написать тест. Мы будем делать простейший запрос без параметров и валидировать ответ.



Retrofit


В качестве библиотеки для сравнения я возьму готовую библиотеку Retrofit, которая есть в OpenAPI Generator и Swagger Codegen. Так выглядит код теста, написанного на Retrofit.



Здесь есть создание клиента: OkHttp сlient из него билдится и настраивается Retrofit-клиент. Это простой тест: мы берём и проверяем, что конкретно у этого запроса статус OK и количество элементов больше нуля.


Давайте рассмотрим тест подробнее. Здесь api. входная точка на наш клиент.



Здесь Story API, мы вызываем метод getInventory и его выполняем.



Здесь же валидация кода ответа. Мы просто проверяем, что код 200.


REST Assured


Давайте напишем тот же тест, но уже на REST Assured. Вот у нас идёт создание клиента, мы его настраиваем, устанавливаем config, устанавливаем mapper, добавляем filter, добавляем BaseUri.



Это простейший тест, он чуть поменьше. Давайте его рассмотрим поподробнее. Что здесь происходит? Есть API-клиент, есть вызов Story API и метод getInventory.



Далее мы используем response specification это особенность REST Assured. И валидируем сразу ответ, проверяем, что код 200.



Два примера очень похожи. Возьмём пример посложнее. Возьмём ручку search.



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



Когда вы начнёте писать тест, то получите тест с множеством null. Он не очень понятен.


В том же REST Assured мы использовали builder-паттерн, и у каждого вызова параметра собственный метод. И тест на REST Assured будет выглядеть вот так.



Добавим ещё один параметр к нашей ручке search. У нас есть спецификация, мы добавляем ещё один параметр, происходит генерация, и наш тест, написанный в Retrofit, ломается. Возникает ошибка компиляции, потому что мы добавили ещё один параметр, о котором мы ничего не знаем конкретно в этом методе.



В REST Assured ошибки не будет, потому что у нас builder-паттерн. Проблемы не возникает.


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


В REST Assured нетипизированные параметры. У Retrofit ответ всегда мапится на ответ из спецификации по умолчанию. В REST Assured ответ может мапиться на ответ из спецификации, а может не мапиться. Потому что, если мы тестируем невалидные кейсы, например, статус-коды не 200, то нам бы хотелось как-то кастомизировать наш ответ, чтобы получать что-то невалидное.


Retrofit не очень удобно кастомизировать. Например, если мне захочется добавить какой-то хедер, то в Retrofit это сделать не очень удобно. В REST Assured у нас есть механизм Request Specification, которым вы можете на любом этапе кастомизировать ваш запрос: как на этапе группы операций, так и на этапе самого запроса.


В данном случае я просто добавил к нашему запросу header x-real-ip.



Вообще, это полезно иметь для любого тестового клиента:


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

REST Assured сlient и Retrofit есть в OpenAPI Generator, их можно попробовать и использовать.


В Swagger Codegen с переходом на другой template-engine остался один Retrofit-клиент. REST Assured-клиента на данный момент нет, но есть открытое issue на его возвращение.


Тяжело ли поддерживать клиент


Главное, для чего создавался клиент, чтобы добавление нового в спецификацию почти никогда не ломало компиляцию клиента. Ключевое слово почти. Понятно, что если мы добавим параметры, как я рассматривал раньше, всё будет хорошо. Но есть некоторые исключения.


Например, рассмотрим редкий случай. Допустим, у нас есть e-num с элементами c одинаковым префиксом. Есть enum, состоящий из следующего: PREFIX_SOLD, PREFIX_AVAIBLE, PREFIX_PENDING.


После генерации сам генератор вырезает этот префикс, и мы уже в тестах используем enum без префикса: SOLD, AVAILABLE, PENDING. Если мы в e-num добавляем значение RETURNED, то после генерации произойдёт следующее: PREFIX возвращается, и наши тесты, которые использовали этот enum без префикса, ломаются. Ломается компиляция. Это первая ситуация за два года, в которой я столкнулся с тем, что когда что-то добавляется, то ломается компиляция.


Удаление или изменение спецификации может сломать компиляцию клиента. Здесь более или менее всё понятно. Допустим, удаляем параметр status. Мы этот параметр используем в тесте, но его уже нет в клиенте. Получаем ошибку компиляции.



Ещё вариант поиск с множеством параметров. Поменяем pet на search. Этого метода в API api.pet нет и получаем ошибку компиляции. Что вполне ожидаемо.



И ещё: изменим в спецификации path. У нас был path /pet/search/, а мы его поменяем на /search/pet/. Что будет?



Поменяется только одна константа внутри нашей операции. И получится, что ошибок компиляции нет. Это неожиданно и ломает все представления об автотестах. Они должны ловить такие случаи, но сейчас получается, что всё работает. Чтобы отлавливать такие случаи, мы используем diff-спецификации, о которых чуть позже.


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


Про тесты и другие возможности


Здесь важно следующее: чем подробнее у нас спецификация, тем больше у нас возможностей.


Например, у наших спецификаций есть модели. Мы можем попробовать сгенерировать assertions на модели ответов с помощью плагина. Получается генерация в кубе. У вас есть в коде модели, эти модели переносятся в OpenAPI-спецификацию, из OpenAPI-спецификации у нас генерятся bean-ы и на эти bean-ы мы генерим assertions.


Например, мы возьмём в качестве библиотеки для генерации assertion такую популярную библиотеку как AssertJ. В AssertJ есть два плагина: есть плагин под Maven, есть плагин под Gradle. С помощью плагина получаем типизированный assertion, который можно использовать.


После настройки этого плагина мы просто указываем пакет, где у нас сгенерированные модели.



Вместо кода, где у нас не типизированные assertions, а стандартные hamcrest матчеры, мы получаем типизированные assertions более удобные и понятные. Если у нас кроме модели ещё есть примеры значений параметров, то можем попробовать сгенерировать реальные шаблоны тестов и сами тесты. Нужно добавить парочку темплейтов, и получим тесты.


Нам пришла идея, почему бы не использовать свои темплейты. Тогда мы получим шаблоны тестов, которые можно использовать и писать. Значения в OpenAPI-спецификации второй версии хранятся в поле x-exаmple. На это поле нет жёстких ограничений.


Какие тесты можно сгенерировать


Для того чтобы настроить генерацию тестов, нам надо прописать template_directory с нужными темплейтами и добавить шаблон для тестов.


Я буду рассматривать контрактные тесты и тесты на сравнение.


Давайте поговорим про контрактные тесты. Мы можем генерировать тесты на статус-коды, можем генерировать тесты на модели и тесты на параметры. Это почти всё, что у нас есть в спецификации по контракту. Здесь я привёл в пример mustache шаблон для генерации теста на статус-коды. Давайте чуть подробнее его рассмотрим. Мы проходим по всем респонсам, пишем бизнес-логику. Я ещё указал Allure-аннотации. Также делаем шаблон для имени теста. Мы устанавливаем в переменные нужные нам значения из спецификации и прокидываем их в вызов реквестов с тестового клиента. В конце добавлена валидация: проверка, что статус код 200.



После генерации у нас в папке target возникают такие тесты. Это реальный тест на 200, его можно запустить.



Код 404 будет выглядеть точно так же. Основная идея, что mustache-темплейты logic-less, и тяжёлую логику нельзя таким образом сгенерировать. И потому это не совсем реальные тесты, а шаблоны тестов. Но дописав логики, вы получите вполне реальные тесты.


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


С моделями всё интересней. Мы можем проверять в тестах, что наш ответ после запроса соответствует определённой модели, которая описана в спецификации. Но в реальности это очень маленькое, узкое покрытие. Мы проверяем только модели и не проверяем значения. Если это, например, JSON, то мы проверяем только поля и что они соответствуют схеме. Но нам бы хотелось понимать, что ответ нашего запроса правильный.


Я обычно предпочитаю тесты на сравнение. Что это и как это работает? Допустим, у нас REST API, который мы тестируем. Рядом поднимаем тот же REST API, но со стабильной версией, о котором мы знаем, что он работает правильно. Делаем два запроса: запрос к REST API, который тестируем, и тот же запрос к стабильному REST API. Получаем два ответа. Первый ответ нам нужно протестировать, второй ответ мы считаем эталоном expected response. Сравнивая два ответа, можем проанализировать, правильно всё работает или нет.


Давайте попробуем написать это в виде шаблонов. Для этого я привёл пример параметров для такого теста. У нас есть механизм в REST Assured Response и Request specification. Здесь параметром выступает Request specification. Мы устанавливаем в Request specification все возможные параметры со значениями, которые описаны у нас в спецификации.



Здесь пример теста, который просто сравнивает эти два ответа. У нас есть функция, которую принимает API-клиент и возвращает ответ, который мы сравниваем через matcher jsonEquals.



Тест после генерации будет выглядеть так. Здесь значения x-example такие же, как и в спецификации. И это получается вполне реальный тест. Осталось добавить реальных тестовых данных и можно его использовать.



Как в итоге мы стали писать тесты


У нас генерируются тестовые классы, мы можем поправить их в target, запустить и использовать. Через IDE нажимаем клавишу F6, и у нас возникает окошко.



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


Что мы получили:


  • Простоту добавления новых тестовых классов. Мы можем легко перенести их в нужные нам тесты и использовать.
  • Единые наименования тестовых методов и классов.

Для тестировщиков головная боль именование тестовых методов и тестовых классов. У нас единое наименование на основе OpenAPI-спецификации. Из-за этого единообразия мы пришли к тому, что наши автотесты могут писать все. Их могут писать автоматизаторы тестирования и понимать ручные тестировщики и разработчики. Всю эту красоту мы вынесли в шаблон проекта с автотестами.


На GitHub создали project-template, где указали клиент, модуль с тестами, настроили генерацию. Если нам нужен проект автотестов на какой-то новый сервис, мы берём и наследуемся от этого шаблона. Чтобы проект заработал, нам достаточно поменять значения двух property, и получаем готовые тесты и сгенерированные клиенты.



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


Об инструментах


Давайте поговорим об инструментах.


Бывает так, что проект автотестов не компилируется из-за изменений спецификации сервиса. Необходимо понимать, почему это происходит. Для этого нам надо получить разницу в документации, например, используя swagger-diff.


Swagger-diff это opensource-проект, который помогает сравнить две спецификации. Я взял его в качестве примера, но есть и другие проекты.


В качестве сравнения берём старую и новую спецификацию: из стабильной версии REST API и тестовой. Далее сравнивая их, мы получаем diff в виде HTML.
Из HTML видим, что удалили параметры search, status и добавили header x-geo.



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


Swagger-coverage. Как его использовать?


С течением времени количество проектов стало расти, REST API постоянно менялись. При этом наши REST API имеют большое количество операций. В самом большом REST API, который я покрывал автотестами, было около 700 операций. Наши автотесты пишут все члены команды: ручные тестировщики, разработчики, автоматизаторы. Ещё мы наняли стажёров, которые тоже пишут тесты и которых тоже нужно контролировать. Возникает вопрос, как вообще понять, что покрыто тестами, а что нет?


Мы решили, что нам нужен инструмент, который позволит измерить это покрытие на основе OpenAPI-спецификации. Я хочу рассказать про Swagger-coverage, который мы недавно реализовали.


Как он работает? У вас есть исходная спецификация. Для каждого реквеста вы можете получить информацию в формате OpenAPI-спецификации. Далее мы агрегируем всю информацию из реквестов, сравниваем с исходной спецификацией и получаем coverage.


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



На странице есть фильтры. Мы знаем, какие операции у нас полностью покрыты, какие частично, какие вообще не покрыты. Также, если мы рассмотрим подробнее операцию, то увидим условия. Эти условия формируются на основе OpenAPI-спецификации. Например, если в OpenAPI-спецификации есть пять кодов ответа, то сформируется пять условий на эти коды ответа. В нашем примере видим, что у нас не проверяются все значения параметра status, мы не используем значение enum-а sold.



Все эти условия настраиваются через Config JSON.


Так же выглядят и теги это группа операций. Мы можем понять, сколько операций покрывается, сколько вообще не покрыты. Ещё есть фильтры по условиям, можем посмотреть, какие условия не покрываются совсем.


Зачем это нужно, и как это использовать? Есть несколько способов использования.


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

Как построить процесс


Теперь давайте построим процесс, объединив всё в одну большую систему.


У нас есть два инструмента: Swagger-diff и Swagger-coverage. Swagger-diff возвращает json с диффом, Swagger-coverage тоже возвращает json о покрытии.


Мы можем сделать такие выводы: что-то изменилось и покрыто тестами или что-то изменилось, но не покрыто тестами.


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


Как построить процесс? У нас собрался build, и запустился Swagger-diff, и мы уже заранее знаем на этом этапе, сломалась ли обратная совместимость и изменилось ли что-нибудь. Это же может показать запуск тестов. Потом мы можем по результатам тестов построить coverage, чтобы понять, что покрыто, а что нет, и на основе этого выбирать дальнейшую стратегию тестирования.


Подведём итог


Что такое Swagger/OAS как основа приёмочных тестов?


  • Всегда актуальный тестовый клиент, понятный для всех членов команды
  • Сгенерированные тесты и шаблоны тестов
  • Лёгкое добавление автотестов на новые проекты, вне зависимости от языка программирования
  • Прозрачность покрытия и изменений REST API через прозрачный инструментарий.

Минутка рекламы: читать расшифровки докладов по тестированию интересно, но ещё интереснее смотреть новые доклады в прямом эфире с возможностью задать вопрос спикеру. Уже на днях, 4 ноября, стартует Heisenbug 2020 Moscow и там докладов по тестированию будет сразу множество.
Подробнее..

Go-swagger как основа взаимодействия микросервисов

04.08.2020 14:20:22 | Автор: admin


Здравствуй, NickName! Если ты программист и работаешь с микросервисной архитектурой, то представь, что тебе нужно настроить взаимодействие твоего сервиса А с каким-то новым и ещё неизвестным тебе сервисом Б. Что ты будешь делать в первую очередь?

Если задать такой вопрос 100 программистам из разных компаний, скорее всего, мы получим 100 разных ответов. Кто-то описывает контракты в swagger, кто-то в gRPC просто делает клиенты к своим сервисам без описания контракта. А кто-то и вовсе хранит JSON в гуглодоке :D. В большинстве компаний складывается свой подход к межсервисному взаимодействию на основании каких-либо исторических факторов, компетенций, стека технологий и прочего. Я хочу рассказать, как сервисы в Delivery Club общаются друг с другом и почему мы сделали именно такой выбор. И главное как мы обеспечиваем актуальность документации с течением времени. Будет много кода!

Ещё раз привет! Меня зовут Сергей Попов, я тим-лид команды, отвечающей за поисковую выдачу ресторанов в приложениях и на сайте Delivery Club, а также активный участник нашей внутренней гильдии разработки на Go (возможно, мы об этом ещё расскажем, но не сейчас).

Сразу оговорюсь, речь пойдет, в основном, про сервисы, написанные на Go. Генерирование кода для PHP-сервисов мы ещё не реализовали, хотя достигаем там единообразия в подходах другим способом.

К чему, в итоге, мы хотели прийти:

  1. Обеспечить актуальность контрактов сервисов. Это должно ускорить внедрение новых сервисов и упростить коммуникацию между командами.
  2. Прийти к единому способу взаимодействия по HTTP между сервисами (пока не будем рассматривать взаимодействия через очереди и event streaming).
  3. Стандартизировать подход к работе с контрактами сервисов.
  4. Использовать единое хранилище контрактов, чтобы не искать доки по всяким конфлюенсам.
  5. В идеале, генерировать клиенты под разные платформы.

Из всего перечисленного на ум приходит Protobuf как единый способ описания контрактов. Он имеет хороший инструментарий и может генерировать клиенты под разные платформы (наш п.5). Но есть и явные недостатки: для многих gRPC остается чем-то новым и неизведанным, а это сильно усложнило бы его внедрение. Ещё одним важным фактором было то, что в компании давно принят подход specification first, и документация уже существовала на все сервисы в виде swagger или RAML-описания.

Go-swagger


Так совпало, что в то же время мы начали адаптацию Go в компании. Поэтому следующим нашим кандидатом на рассмотрение оказался go-swagger инструмент, который позволяет генерировать клиентов и серверный код из swagger-спецификации. Из очевидных недостатков он генерирует код только для Go. На самом деле, там используется гошное кодогенерирование, и go-swagger позволяет гибко работать с шаблонам, так что, теоретически, его можно использовать для генерирования кода на PHP, но мы ещё не пробовали.

Go-swagger это не только про генерирование транспортного слоя. Фактически он генерирует каркас приложения, и тут я бы хотел немного упомянуть о культуре разработки в DC. У нас есть Inner Source, а это значит, что любой разработчик из любой команды может создать pull request в любой сервис, который у нас есть. Чтобы такая схема работала, мы стараемся стандартизировать подходы в разработке: используем общую терминологию, единый подход к логированию, метрикам, работе с зависимостями и, конечно же, к структуре проекта.

Таким образом, внедряя go-swagger, мы вводим стандарт разработки наших сервисов на Go. Это еще один шаг навстречу нашим целям, на который мы изначально не рассчитывали, но который важен для разработки в целом.

Первые шаги


Итак, go-swagger оказался интересным кандидатом, который, кажется, может покрыть большинство наших хотелок требований.
Примечание: весь дальнейший код актуален для версии 0.24.0, инструкцию по установке можно посмотреть в нашем репозитории с примерами, а на официальном сайте есть инструкция по установке актуальной версии.
Давайте посмотрим, что он умеет. Возьмём swagger-спеку и сгенерируем сервис:

> goswagger generate server \    --with-context -f ./swagger-api/swagger.yml \    --name example1

Получилось у нас следующее:



Makefile и go.mod я уже сделал сам.

Фактически у нас получился сервис, который обрабатывает запросы, описанные в swagger.

> go run cmd/example1-server/main.go2020/02/17 11:04:24 Serving example service at http://127.0.0.1:54586   > curl http://localhost:54586/hello -iHTTP/1.1 501 Not ImplementedContent-Type: application/jsonDate: Sat, 15 Feb 2020 18:14:59 GMTContent-Length: 58Connection: close "operation hello HelloWorld has not yet been implemented"

Второй шаг. Разбираемся с шаблонизацией


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

Что мы хотим от структуры нашего приложения:

  • Уметь конфигурировать приложение: передавать настройки подключения к БД, указывать порт HTTP-соединений и прочее.
  • Выделить объект приложения, который будет хранить состояние приложения, подключение к БД и прочее.
  • Сделать хэндлеры функциями нашего приложения, это должно упростить работу с кодом.
  • Инициализировать зависимости в main-файле (в нашем примере этого не будет, но мы всё равно этого хотим.

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



Нам необходимо описать файлы шаблонов (`*.gotmpl`) и файл для конфигурации (`*.yml`) генерирования нашего сервиса.

Далее по порядку разберем те шаблоны, которые сделал я. Глубоко погружаться в работу с ними не буду, потому что документация go-swagger достаточно подробная, например, вот описание файла конфигурации. Отмечу только, что используется Go-шаблонизация, и если у вас уже есть в этом опыт или приходилось описывать HELM-конфигурации, то разобраться не составит труда.

Конфигурирование приложения


config.gotmpl содержит простую структуру с одним параметром портом, который будет слушать приложение для входящих HTTP-запросов. Также я сделал функцию InitConfig, которая будет считывать переменные окружения и заполнять эту структуру. Вызывать буду из main.go, поэтому InitConfig сделал публичной функцией.

package config import (    "github.com/pkg/errors"    "github.com/vrischmann/envconfig") // Config structtype Config struct {    HTTPBindPort int `envconfig:"default=8001"`} // InitConfig funcfunc InitConfig(prefix string) (*Config, error) {    config := &Config{}    if err := envconfig.InitWithPrefix(config, prefix); err != nil {        return nil, errors.Wrap(err, "init config failed")    }     return config, nil}

Чтобы этот шаблон использовался при генерировании кода, его нужно указать в YML-конфиге:

layout:  application:    - name: cfgPackage      source: serverConfig      target: "./internal/config/"      file_name: "config.go"      skip_exists: false

Немного расскажу про параметры:

  • name несёт чисто информативную функцию и на генерирование не влияет.
  • source фактически путь до файла шаблона в camelCase, т.е. serverConfig равносильно ./server/config.gotmpl.
  • target директория, куда будет сохранен сгенерированный код. Здесь можно использовать шаблонизацию для динамического формирования пути (пример).
  • file_name название сгенерированного файла, здесь также можно использовать шаблонизацию.
  • skip_exists признак того, что файл будет сгенерирован только один раз и не будет перезаписывать существующий. Для нас это важно, потому что файл конфига будет меняться по мере роста приложения и не должен зависеть от генерируемого кода.

В конфиге кодогенерирования нужно указывать все файлы, а не только те, которые мы хотим переопределить. Для файлов, которые мы не меняем, в значении source указываем asset:<путь до шаблона>, например, как здесь: asset:serverConfigureapi. Кстати, если интересно посмотреть оригинальные шаблоны, то они здесь.

Объект приложения и хэндлеры


Объект приложения для хранения состояния, подключений БД и прочего я описывать не буду, всё аналогично только что сделанному конфигу. А вот с хэндлерами всё немного интереснее. Наша ключевая цель состоит в том, чтобы при добавлении URL в спецификацию у нас в отдельном файле создалась функция с заглушкой, и самое главное, чтобы наш сервер вызывал эту функцию для обработки запроса.

Опишем шаблон функции и заглушки:

package app import (    api{{ pascalize .Package }} "{{.GenCommon.TargetImportPath}}/{{ .RootPackage }}/operations/{{ .Package }}"    "github.com/go-openapi/runtime/middleware") func (srv *Service){{ pascalize .Name }}Handler(params api{{ pascalize .Package }}.{{ pascalize .Name }}Params{{ if .Authorized }}, principal api{{ .Package }}.{{ if not ( eq .Principal "interface{}" ) }}*{{ end }}{{ .Principal }}{{ end }}) middleware.Responder {    return middleware.NotImplemented("operation {{ .Package }} {{ pascalize .Name }} has not yet been implemented")}

Немного разберём пример:

  • pascalize приводит строку с CamelCase (описание остальных функции здесь).
  • .RootPackage пакет сгенерированного веб-сервера.
  • .Package название пакета в сгенерированном коде, в котором описаны все необходимые структуры для HTTP-запросов и ответов, т.е. структуры. Например, структура для тела запроса или структура ответа.
  • .Name название хэндлера. Оно берётся из operationID в спецификации, если указано. Я рекомендую всегда указывать operationID для более очевидного результата.

Конфиг для хэндлера следующий:

layout:  operations:    - name: handlerFns      source: serverHandler      target: "./internal/app"      file_name: "{{ (snakize (pascalize .Name)) }}.go"      skip_exists: true

Как видите, код хэндлеров не будет перезаписываться (skip_exists: true), а название файла будет генерироваться из названия хэндлера.

Окей, функция с заглушкой есть, но веб-сервер ещё не знает, что эти функции нужно использовать для обработки запросов. Я исправил это в main.go (весь код приводить не буду, полную версию можно найти здесь):

package main {{ $name := .Name }}{{ $operations := .Operations }}import (    "fmt"    "log"     "github.com/delivery-club/go-swagger-example/{{ dasherize .Name }}/internal/generated/restapi"    "github.com/delivery-club/go-swagger-example/{{ dasherize .Name }}/internal/generated/restapi/operations"    {{range $index, $op := .Operations}}        {{ $found := false }}        {{ range $i, $sop := $operations }}            {{ if and (gt $i $index ) (eq $op.Package $sop.Package)}}                {{ $found = true }}            {{end}}        {{end}}        {{ if not $found }}        api{{ pascalize $op.Package }} "{{$op.GenCommon.TargetImportPath}}/{{ $op.RootPackage }}/operations/{{ $op.Package }}"        {{end}}    {{end}}     "github.com/go-openapi/loads"    "github.com/vrischmann/envconfig"     "github.com/delivery-club/go-swagger-example/{{ dasherize .Name }}/internal/app") func main() {    ...    api := operations.New{{ pascalize .Name }}API(swaggerSpec)     {{range .Operations}}    api.{{ pascalize .Package }}{{ pascalize .Name }}Handler = api{{ pascalize .Package }}.{{ pascalize .Name }}HandlerFunc(srv.{{ pascalize .Name }}Handler)    {{- end}}    ...}

Код в импорте выглядит сложным, хотя на самом деле здесь просто Go-шаблонизация и структуры из репозитория go-swagger. А в функции main мы просто присваиваем хэндлерам наши сгенерированные функции.

Осталось сгенерировать код с указанием нашей конфигурации:

> goswagger generate server \        -f ./swagger-api/swagger.yml \        -t ./internal/generated -C ./swagger-templates/default-server.yml \        --template-dir ./swagger-templates/templates \        --name example2

Финальный результат можно посмотреть в нашем репозитории.

Что мы получили:

  • Мы можем использовать свои структуры для приложения, конфигов и всего, что захотим. Самое главное это достаточно просто встраивается в генерируемый код.
  • Мы можем гибко управлять структурой проекта, вплоть до названий отдельных файлов.
  • Go-шаблонизация выглядит сложной и к ней нужно привыкнуть, но в целом это очень мощный инструмент.

Третий шаг. Генерирование клиентов


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

Для проектов на Go принято складывать публичные пакеты в ./pkg, мы сделаем так же: положим клиент для нашего сервиса в pkg, а сам код сгенерируем следующим образом:

> goswagger generate client -f ./swagger-api/swagger.yml -t ./pkg/example3

Пример сгенерированного кода здесь.

Теперь все потребители нашего сервиса могут импортировать себе этот клиент, например, по тэгу (для моего примера тэг будет example3/pkg/example3/v0.0.1).

Шаблоны клиентов можно настраивать, чтобы, например, прокидывать open tracing id из контекста в заголовок.

Выводы


Естественно, наша внутренняя реализация отличается от приведенного здесь кода, в основном, за счёт использования внутренних пакетов и подходов к CI (запуск различных тестов и линтеров). В сгенерированном коде из коробки настроен сбор технических метрик, работа с конфигами и логирование. Мы стандартизировали все общие инструменты. За счёт этого мы упростили разработку в целом и выпуск новых сервисов в частности, обеспечили более быстрое прохождение чек-листа сервиса перед деплоем на прод.

Давайте проверим, получилось ли достигнуть первоначальных целей:

  1. Обеспечить актуальность описанных для сервисов контрактов, это должно ускорить внедрение новых сервисов и упростить коммуникацию между командами Да.
  2. Прийти к единому способу взаимодействия по HTTP между сервисами (пока не будем рассматривать взаимодействия через очереди и event streaming) Да.
  3. Стандартизировать подход к работе с контрактами сервисов, т.к. мы давно пришли к подходу Inner Source в разработке сервисов Да.
  4. Использовать единое хранилище контрактов, чтобы не искать документацию по всяким конфлюенсам Да (фактически Bitbucket).
  5. В идеале, генерировать клиенты под разные платформы Нет (на самом деле, не пробовали, шаблонизация не ограничивает в этом плане).
  6. Внедрить стандартную структуру сервиса на Go Да (дополнительный результат).

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

P.S. Если этот материал понравится, то в дальнейшем подготовим статью про стандартную архитектуру наших сервисов, расскажем, какими принципами мы пользуемся при разработке сервисов на Go.
Подробнее..

Перевод Описание элементов перечислений в Swashbuckle

16.04.2021 14:07:36 | Автор: admin

Swagger замечательная вещь! Он позволяет легко посмотреть, каким API обладает ваш сервис, сгенерировать клиента для него на различных языках и даже попробовать поработать с сервисом через UI. В ASP.NET Core для поддержки Swagger существует пакет Swashbuckle.AspNetCore.


Но есть один недостаток, который мне не нравится. Swashbuckle способен строить описания методов, параметров и классов, основываясь на XML-комментариях в коде .NET. Но он не показывает те описания, которые применяются непосредственно к членам перечислений.


Позвольте мне показать, о чём идёт речь.


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


Я создал простой Web-сервис:


/// <summary>/// Contains endpoints that use different enums./// </summary>[Route("api/data")][ApiController]public class EnumsController : ControllerBase{    /// <summary>    /// Executes operation of requested type and returns result status.    /// </summary>    /// <param name="id">Operation id.</param>    /// <param name="type">Operation type.</param>    /// <returns>Result status.</returns>    [HttpGet]    public Task<Result> ExecuteOperation(int id, OperationType type)    {        return Task.FromResult(Result.Success);    }    /// <summary>    /// Changes data    /// </summary>    [HttpPost]    public Task<IActionResult> Change(DataChange change)    {        return Task.FromResult<IActionResult>(Ok());    }}

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


/// <summary>/// Operation types./// </summary>public enum OperationType{    /// <summary>    /// Do operation.    /// </summary>    Do,    /// <summary>    /// Undo operation.    /// </summary>    Undo}/// <summary>/// Operation results./// </summary>public enum Result{    /// <summary>    /// Operations was completed successfully.    /// </summary>    Success,    /// <summary>    /// Operation failed.    /// </summary>    Failure}/// <summary>/// Data change information./// </summary>public class DataChange{    /// <summary>    /// Data id.    /// </summary>    public int Id { get; set; }    /// <summary>    /// Source type.    /// </summary>    public Sources Source { get; set; }    /// <summary>    /// Operation type.    /// </summary>    public OperationType Operation { get; set; }}/// <summary>/// Types of sources./// </summary>public enum Sources{    /// <summary>    /// In-memory data source.    /// </summary>    Memory,    /// <summary>    /// Database data source.    /// </summary>    Database}

Для поддержки Swagger я установил в проект NuGet-пакет Swashbuckle.AspNetCore. Теперь его нужно подключить. Это делается в Startup-файле:


public class Startup{    // This method gets called by the runtime. Use this method to add services to the container.    public void ConfigureServices(IServiceCollection services)    {        services.AddControllers();        services.AddSwaggerGen(c => {            // Set the comments path for the Swagger JSON and UI.            var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";            var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);            c.IncludeXmlComments(xmlPath);        });    }    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)    {        if (env.IsDevelopment())        {            app.UseDeveloperExceptionPage();        }        app.UseSwagger();        app.UseSwaggerUI();        app.UseRouting();        ...    }}

Теперь мы можем запустить наше приложение, и по адресу http://localhost:5000/swagger/index.html мы найдём описание нашего сервиса:


Swagger UI для приложения


Но пока наши перечисления представлены просто числами:


Представление перечислений числами


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


Для этого нам нужно внести небольшие изменения в настройку нашего сервиса. Я установил NuGet-пакет Swashbuckle.AspNetCore.Newtonsoft. После этого, я чуть изменил настройки сервисов. Я заменил


services.AddControllers();

на


services.AddControllers().AddNewtonsoftJson(o =>{    o.SerializerSettings.Converters.Add(new StringEnumConverter    {        CamelCaseText = true    });});

Теперь наши перечисления представлены в виде строк:


Представление перечислений строками


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


Описание типов перечислений


Давайте посмотрим, как нам вернуть их. Большей частью поиск в интернете на эту тему ничего не дал мне. Но, в конце концов, я нашёл интересный код. К сожалению, он относится к старой версии Swashbuckle. Но он послужил мне хорошей отправной точкой.


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


public class EnumTypesSchemaFilter : ISchemaFilter{    private readonly XDocument _xmlComments;    public EnumTypesSchemaFilter(string xmlPath)    {        if(File.Exists(xmlPath))        {            _xmlComments = XDocument.Load(xmlPath);        }    }    public void Apply(OpenApiSchema schema, SchemaFilterContext context)    {        if (_xmlComments == null) return;        if(schema.Enum != null && schema.Enum.Count > 0 &&            context.Type != null && context.Type.IsEnum)        {            schema.Description += "<p>Members:</p><ul>";            var fullTypeName = context.Type.FullName;            foreach (var enumMemberName in schema.Enum.OfType<OpenApiString>().Select(v => v.Value))            {                var fullEnumMemberName = $"F:{fullTypeName}.{enumMemberName}";                var enumMemberComments = _xmlComments.Descendants("member")                    .FirstOrDefault(m => m.Attribute("name").Value.Equals(fullEnumMemberName, StringComparison.OrdinalIgnoreCase));                if (enumMemberComments == null) continue;                var summary = enumMemberComments.Descendants("summary").FirstOrDefault();                if (summary == null) continue;                schema.Description += $"<li><i>{enumMemberName}</i> - {summary.Value.Trim()}</li>";            }            schema.Description += "</ul>";        }    }}

Конструктор этого класса принимает имя файла с XML-комментариями. Его содержимое для удобства работы читается в экземпляр XDocument. Затем в методе Apply мы проверяем, генерируется ли схема для перечисления или нет. Если это схема перечисления, то мы добавляем к описанию класса HTML-список, содержащий описание каждого используемого члена перечисления.


Теперь наш класс нужно подключить, чтобы Swashbuckle знал о нём:


services.AddSwaggerGen(c => {    // Set the comments path for the Swagger JSON and UI.    var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";    var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);    c.IncludeXmlComments(xmlPath);    c.SchemaFilter<EnumTypesSchemaFilter>(xmlPath);});

Это делается с помощью метода SchemaFilter в настройках Swagger. Этому методу в качестве параметра передаётся имя файла с XML-комментариями. Именно оно будет передано в конструктор нашего класса EnumTypesSchemaFilter.


Теперь в Swagger UI описания классов-перечислений выглядят так:


XML-комментарии к членам перечисления


Описание перечислений в параметрах


Это уже лучше. Но всё же не достаточно хорошо. У нас в контроллере есть метод, который принимает перечисление в качестве параметра:


public Task<Result> ExecuteOperation(int id, OperationType type)

Давайте посмотрим, как выглядит его описание в Swagger UI:


Описание параметра


Как видите, здесь не присутствует никакого описания членов перечисления. Причина в том, что здесь мы видим описание параметра, а не его типа. Т.е. вы видите XML-комментарий, соответствующий параметру метода, а не типу этого параметра.


Но и эту проблему можно решить. Для этого воспользуемся другим интерфейсом Swashbuckle IDocumentFilter. Вот его реализация:


public class EnumTypesDocumentFilter : IDocumentFilter{    public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)    {        foreach (var path in swaggerDoc.Paths.Values)        {            foreach(var operation in path.Operations.Values)            {                foreach(var parameter in operation.Parameters)                {                    var schemaReferenceId = parameter.Schema.Reference?.Id;                    if (string.IsNullOrEmpty(schemaReferenceId)) continue;                    var schema = context.SchemaRepository.Schemas[schemaReferenceId];                    if (schema.Enum == null || schema.Enum.Count == 0) continue;                    parameter.Description += "<p>Variants:</p>";                    int cutStart = schema.Description.IndexOf("<ul>");                    int cutEnd = schema.Description.IndexOf("</ul>") + 5;                    parameter.Description += schema.Description                        .Substring(cutStart, cutEnd - cutStart);                }            }        }    }}

Здесь в методе Apply мы перебираем все параметры всех методов всех контроллеров. К сожалению, здесь Swashbuckle API не даёт доступа в типу параметра, а только к схеме его типа (ну или я не нашёл такой возможности). Поэтому мне пришлось вырезать описание членов перечисления из строки описания типа параметра.


Наш класс нужно зарегистрировать аналогичным образом, с помощью метода DocumentFilter:


services.AddSwaggerGen(c => {    // Set the comments path for the Swagger JSON and UI.    var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";    var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);    c.IncludeXmlComments(xmlPath);    c.SchemaFilter<EnumTypesSchemaFilter>(xmlPath);    c.DocumentFilter<EnumTypesDocumentFilter>();});

Вот как выглядит теперь описание параметра в Swagger UI:


Описание параметра с доступными вариантами


Заключение


Приведённый здесь код является скорее наброском для решения проблемы, чем окончательным вариантом. Надеюсь, он будет полезен вам и позволит добавить описание членов перечисления в ваш Swagger UI. Спасибо!


P.S. Вы можете найти код проекта на GitHub.

Подробнее..
Категории: Net , Swagger , Swagger-ui

Что было раньше код или документация? OpenApi (OAS 3.0) и проблемы кодогенерации на Java

12.11.2020 14:14:33 | Автор: admin

Задача интеграции сервисов и различных систем является чуть ли не одной из основных проблем современного IT. На сегодняшний день самым популярным архитектурным стилем для проектирования распределенных систем является REST. Но, как известно, единого стандарта для RESTfulсервисов нет, а у кажущейся простоты и свободы есть обратная сторона медали. Не менее важной является задача интеграции систем разными командами или даже компаниями, с которой приходит вопрос об актуальности документации на протяжении жизни всего проекта, и удобного способа передачи потребителю.

Эту проблему отчасти удалось решить при помощи спецификации OpenAPI(OAS3.0)[1], но все равно часто встает вопрос о правильном применении и подводных камнях кодогенерации, например на языке Java. И можно ли полностью предоставить аналитикам написание функциональных требований, документации и моделей в ymlформе для OpenAPI, а разработчикам предоставить возможность только написание бизнес-логики?

Что было раньше: код или документация?

Я уже около десяти лет занимаюсь разработкой на Java,и почти в каждом проекте так или иначе приходилось сталкиваться с необходимостью поделиться разработанным сервисом с другими людьми. Иногда достаточно просто составить понятное описание APIсуществующего сервиса, но в серьезной enterprise-разработке такое описание часто создается еще на этапе аналитики в виде полноценного документа и является основой для грядущей работы.

В некоторых случаях такие документы составляются на этапе проектирования архитектуры в обычном текстовом формате, а потом передаются разработчику для реализации. Однако, для разработки REST-APIтакой подход не очень удобен и усложняет жизнь как программистам, которые должны еще правильно понять написанное, так и командам, которые будут с получившимся продуктом интегрироваться.К тому же документы могут интерпретироваться людьми по-разному, что обязательно приведет к необходимости дополнительных обсуждений, согласований и переделок, что в конечном счете срывает сроки и добавляет дефекты в продукт. Хотя, метод не очень удобен, иногда он до сих пор применяется при проектировании проектов без OpenAPI(например файловая интеграция, интеграция через БД, интеграция через очереди и общую шину).

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

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

В случае крупных и серьезных проектов, ручное документирование приводит к ряду больших проблем. В SOAPсервисах потребители, получая ссылку на WSDL-документ, генерируют на своей стороне модель данных и клиентский код. В отличие от SOAPпри интеграции OAS3.0 или OpenAPISpecificationявляется не просто спецификацией, а целой экосистемой инструментов для создания RESTсервисов, и она позволяет решить как задачу генерации документации по существующему коду, так и обратную - генерацию кода по документации. Нопрежде чем ответить на вечный вопрос, что первично, нужно определиться с целями и потребностями проекта.

Генерация документации по коду

Первым рассмотрим подход с автодокументируемым кодом[2,3]. Простота здесь заключается в том, что при использовании SpringMVCот разработчиков требуется всего лишь подключить пару библиотек, и на выходе уже получается вполне сносная документация с возможностью интерактивного взаимодействия с API.

Зависимости:

Spoiler

Maven

<dependency>   <groupId>org.springdoc</groupId>   <artifactId>springdoc-openapi-ui</artifactId>   <version>1.4.8</version></dependency>
<dependency>   <groupId>org.springdoc</groupId>   <artifactId>springdoc-openapi-webflux-ui</artifactId>   <version>1.4.8</version></dependency>

Gradle

compile group: 'org.springdoc', name: 'springdoc-openapi-ui', version: '1.4.8'
compile group: 'org.springdoc', name: 'springdoc-openapi-webflux-ui', version: '1.4.8'

Результат доступен на: http://localhost:8080/swagger-ui.html

Сгенерированная документация по ссылке: http://localhost:8080/v3/api-docs

Плюсы подхода:

  • Возможность быстро сгенерировать документацию по существующему коду

  • Простота интеграции

  • Актуальная онлайн документация

  • Встроенная возможность интерактивного взаимодействия

Минусы подхода:

  • Документация написанная для разработчиков, содержит немного бизнес информации

  • При требовании подробного описания моделей и API возникает необходимость внедрять в код аннотации.

  • Для любых изменений требуется участие разработчика и правки кода

  • Требует от разработчика полного понимания архитектуры приложения или дополнительного документа с ее описанием

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

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

Генерация кода по документации

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

Плюсы подхода

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

  • Есть возможность разделить код и документацию

  • Можно объединить описание моделей, API и функциональных/бизнес требований

  • Любые изменения, инициированные бизнесом, сразу же применяются к модели и контроллерам

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

  • Документацию в yaml формате легко предоставить потребителям сервиса при помощи сторонних библиотек, например redoc[4]

  • На основе yaml файлов можно описать сервисы на стороне потребителя, например, OpenApi для получения нотификаций от основного сервиса

  • На основе этой документации генерировать у себя тестовые сервисы для интеграционных и функциональных тестов

Минусы подхода

  • Более актуально для крупных и долгоживущих проектов (от одного года разработки)

  • Требует знания yaml формата для описания моделей

  • Требует навыки работы с markdown для описания функциональных требований

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

Подводные камни и опыт реализации:

Задача по применению второго подхода впервые появилась, когда потребовалось предоставить потребителям актуальную веб-версию документации с подробным описанием функциональных требований и бизнес-кейсов. Написание такой документации и ее актуализацию предстояло вести техническим аналитикам без возможности напрямую редактировать код. И еще одним важным требованием было красивое визуальное представление документации. Далее опишу базовый набор конфигураций для достижения этой цели и возможные подводные камни и проблемы[2,3]

Первым делом добавляем зависимость на mavenплагин

Spoiler
<dependency>   <groupId>io.swagger.codegen.v3</groupId>   <artifactId>swagger-codegen-maven-plugin</artifactId>   <version>3.0.21</version></dependency>
   <artifactId>springdoc-openapi-webflux-ui</artifactId>
   <version>1.4.8</version>

И конфигурируем генератор кода

Spoiler
<plugin>   <groupId>io.swagger.codegen.v3</groupId>   <artifactId>swagger-codegen-maven-plugin</artifactId>   <version>3.0.21</version>   <executions>       <execution>           <id>1</id>           <goals>               <goal>generate</goal>           </goals>           <configuration>               <groupId>com.habr</groupId>               <artifactId>oas3</artifactId>               <inputSpec>${project.basedir}/src/main/resources/habr-1.yaml</inputSpec>               <output>${project.build.directory}/generated-sources/</output>               <language>spring</language>               <configOptions>                   <sourceFolder>src/gen/java/main</sourceFolder>                   <library>spring-mvc</library>                   <interfaceOnly>true</interfaceOnly>                   <useBeanValidation>true</useBeanValidation>                   <dateLibrary>java8</dateLibrary>                   <java8>true</java8>                   <apiTests>false</apiTests>                   <modelTests>false</modelTests>               </configOptions>               <modelPackage>com.habr.oas3.model</modelPackage>               <apiPackage>com.habr.oas3.controller</apiPackage>           </configuration>       </execution>       <execution>...</execution>     ...   </executions>

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

InputSpecсодержит путь к документации, а modelPackageи apiPackageпакеты для сгенерированных моделей. Также можно генерировать моковые реализации интерфейсов для тестов (при генерации некоторый контроллер, что отвечает типичным ответом).

Флаг interfaceOnlyпозволяет генерировать только интерфейсы контроллеров и предоставить разработчику лишь контракты для реализации.

Общая структура yaml документации:

  1. openApi - Содержит версию

  2. info - метаданные для api

  3. servers - информация о серверах c api

  4. tags - дополнительные метаданные, в этом блоке нужно писать текст ФТ и регламента взаимодействия

  5. paths - описание endpoints и интерфейсов контроллеров

  6. components - описание модели данных

  7. security - схема безопасности

Исходный код на github и пример документации можно посмотреть здесь

Визуализация и предоставление потребителю: swagger-ui vs redoc

Теперь поговорим про способы поставки полученной документации. Помимо ручного написания и передачи любым удобным каналом бытует подход с онлайн-документацией в связке с существующим сервисом. Рассмотрим самые удобные и популярный способы такой поставки.

Swagger-ui

Удобная реализация с возможностью интерактивного взаимодействия. Идет из коробки в проекте swagger.io[5]. Можно подключить как библиотеку вместе с сервисом или развернуть статикой отдельно.

Пример визуализации документации тестового сервиса:

Redoc

Он позволяет отобразить и кастомизировать документацию альтернативным способом. Более удобная и красивая структура документации[4].

Пример визуализации документации тестового сервиса:

Хотя, redocреализован на react, его также можно использовать и как VueJSкомпонент:

Spoiler
<template>    <div v-if="hasYmlContent">        <RedocStandalone :spec='ymlObject' :options='ropt' />    </div></template><style...><script>    import AuthService from '../services/auth.service';    import DocumentationService from '../services/documentation.service'    import {RedocStandalone} from 'redoc'    import {API_URL} from "../services/auth-header";    import YAML from "yamljs"    import Jquery from "jquery"    export default {        name: 'documentation',        components: {'RedocStandalone': RedocStandalone},        props: ['yml'],        data: function() {            return {                ymlContent: false,                ropt:{...},            }        },        created() {...},        computed: {...}    };</script>

Выводы

Проведение аналитики и проектирование системы до начала этапа разработки помогает лучше понять задачи и избежать многих ошибок при реализации. Проект OAS3.0 показал себя очень удобным для описания документации и поставки ее потребителям сервиса.

На опыте своих проектов удалось убедиться, что такой подход не только удобен, но и позволяет без особых сложностей передавать аналитикам часть работы с описанием моделей и APIбудущей системы, что также повышает их собственную экспертизу и понимание сервиса. Освоение ведения такой документации довольно несложно, и, как показала практика, даже человек без опыта использования этого подхода уже через пару недель полноценно освоил не только ведение документации в ymlформате, поставку ее через git, но и оценил его удобство против классического ручного составление функциональных требований.

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

Источники

  1. https://swagger.io/specification/

  2. https://www.baeldung.com/spring-rest-openapi-documentation

  3. https://github.com/springdoc/springdoc-openapi

  4. https://redocly.github.io/redoc/

  5. https://swagger.io/tools/swagger-ui/

Подробнее..

Документирование API в Java приложении с помощью Swagger v3

07.01.2021 18:21:18 | Автор: admin

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


Создание документации вручную утомительный процесс. Swagger поможет вам упростить эту работу.


Что такое Swagger?


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


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


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


Вы найдете все примеры представленные тут в моем репозитории.


Создание REST API


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


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


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


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


public class UserDto {    private String key;    private String name;    private Long points = 0L;    private Gender gender;    private LocalDateTime regDate = LocalDateTime.now();    public UserDto() {    }    public UserDto(String key, String name, Gender gender) {        this.key = key;        this.name = name;        this.gender = gender;    }    public static UserDto of(String key, String value, Gender gender) {        return new UserDto(key, value, gender);    }    // getters and setters}

public enum Gender {    MAN, WOMAN}

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


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


@RestController@RequestMapping("/api/user")public class UserController {    private final Map<String, UserDto> repository;    public UserController(Map<String, UserDto> repository) {        this.repository = repository;    }    @PutMapping(produces = APPLICATION_JSON_VALUE)    public HttpStatus registerUser(@RequestBody UserDto userDto) {        repository.put(userDto.getKey(), userDto);        return HttpStatus.OK;    }    @PostMapping(produces = APPLICATION_JSON_VALUE)    public HttpStatus updateUser(@RequestBody UserDto userDto) {        if (!repository.containsKey(userDto.getKey())) return HttpStatus.NOT_FOUND;        repository.put(userDto.getKey(), userDto);        return HttpStatus.OK;    }    @GetMapping(value = "{key}", produces = APPLICATION_JSON_VALUE)    public ResponseEntity<UserDto> getSimpleDto(@PathVariable("key") String key) {        return ResponseEntity.ok(repository.get(key));    }}

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


@RestController@RequestMapping("api/user/point")public class PointController {    private final Map<String, UserDto> repository;    public PointController(Map<String, UserDto> repository) {        this.repository = repository;    }    @PostMapping("{key}")    public HttpStatus changePoints(            @PathVariable String key,            @RequestPart("point") Long point,            @RequestPart("type") String type    ) {        final UserDto userDto = repository.get(key);        userDto.setPoints(                "plus".equalsIgnoreCase(type)                     ? userDto.getPoints() + point                     : userDto.getPoints() - point        );        return HttpStatus.OK;    }}

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


@RestController@RequestMapping("api/secret")public class SecretController {    private final Map<String, UserDto> repository;    public SecretController(Map<String, UserDto> repository) {        this.repository = repository;    }    @GetMapping(value = "destroy")    public HttpStatus destroy() {        repository.clear();        return HttpStatus.OK;    }}

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


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


<dependency>    <groupId>io.swagger.core.v3</groupId>    <artifactId>swagger-annotations</artifactId>    <version>2.1.6</version></dependency><dependency>    <groupId>org.springdoc</groupId>    <artifactId>springdoc-openapi-ui</artifactId>    <version>1.5.2</version></dependency>

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


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


Поэтому после добавления зависимостей, у нас уже есть документация. Чтобы убедиться в этом, переходим по адресу: localhost:8080/swagger-ui.html


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


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



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


Для начала создадим класс конфигурации сваггера SwaggerConfig имя произвольное.


@Configurationpublic class SwaggerConfig {    @Bean    public OpenAPI customOpenAPI() {        return new OpenAPI()                .info(                        new Info()                                .title("Example Swagger Api")                                .version("1.0.0")                );    }}

  • title это название вашего приложения
  • version версия вашего API

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


Добавление авторов


Добавьте разработчиков API, чтобы было понятно, кто в ответе за это безобразие


@Beanpublic OpenAPI customOpenAPI() {    return new OpenAPI()            .info(                    new Info()                            .title("Loyalty System Api")                            .version("1.0.0")                            .contact(                                    new Contact()                                            .email("me@upagge.ru")                                            .url("https://uPagge.ru")                                            .name("Struchkov Mark")                            )            );}

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


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


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

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


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


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


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

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


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


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


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


  • summary короткое описание.
  • description более полное описание.

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

Метод с аннотацией Operation


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


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


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

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


Разметка DTO


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


@Schema(description = "Сущность пользователя")public class UserDto {    @Schema(description = "Идентификатор")    private String key;    // ... ... ... ... ...}

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


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

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


Разметка аннотацией Schema


Разметка аннотацией Schema


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


public class UserDto {    @Schema(accessMode = Schema.AccessMode.READ_ONLY)    private String key;    ...}

Валидация


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


Подробнее о валидации данных в этой статье.


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

Давайте посмотрим на изменения спецификации. Для поля point появилось замечание minimum: 0.


Валидация в swagger


И все это нам не стоило ни малейшего дополнительного усилия.


Итог


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


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

Подробнее..

Swagger (OpenAPI 3.0)

09.02.2021 14:04:30 | Автор: admin

Всем привет!!! Это мой первый пост на Хабре и я хочу поделиться с вами своим опытом в исследование нового для себя фреймворка.

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

Сейчас хочу поделиться ею в надежде, что кому-то она поможет в изучение Swagger (OpenApi 3.0)

Введение

Я на 99% уверен у многих из вас были проблемы с поиском документации для нужного вам контроллера. Многие если и находили ее быстро, но в конечном итоге оказывалось что она работает не так как описано в документации, либо вообще его уже нет.
Сегодня я вам докажу, что есть способы поддерживать документацию в актуальном виде и в этом мне будет помогать Open Source framework от компании SmartBear под названием Swagger, а с 2016 года он получил новое обновление и стал называться OpenAPI Specification.

Swagger - это фреймворк для спецификации RESTful API. Его прелесть заключается в том, что он дает возможность не только интерактивно просматривать спецификацию, но и отправлять запросы так называемый Swagger UI.

Также возможно сгенерировать непосредственно клиента или сервер по спецификации API Swagger, для этого понадобиться Swagger Codegen.

Основные подходы

Swagger имеет два подхода к написанию документации:

  • Документация пишется на основании вашего кода.

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

    • Код проекта становиться не очень читабельным от обилия аннотаций и описания в них.

    • Вся документация будет вписана в нашем коде (все контроллеры и модели превращаются в некий Java Swagger Code)

    • Подход не советуют использовать, если есть возможности, но его очень просто интегрировать.

  • Документация пишется отдельно от кода.

    • Данный подход требует знать синтаксис Swagger Specification.

    • Документация пишется либо в JAML/JSON файле, либо в редакторе Swagger Editor.

Swagger Tools

Swagger или OpenAPI framework состоит из 4 основных компонентов:

  1. Swagger Core - позволяет генерировать документацию на основе существующего кода основываясь на Java Annotation.

  2. Swagger Codegen - позволит генерировать клиентов для существующей документации.

  3. Swagger UI - красивый интерфейс, который представляет документацию. Дает возможность просмотреть какие типы запросов есть, описание моделей и их типов данных.

  4. Swagger Editor - Позволяет писать документацию в YAML или JSON формата.

Теперь давайте поговорим о каждом компоненте отдельно.

Swagger Core

Swagger Code - это Java-реализация спецификации OpenAPI

Для того что бы использовать Swagger Core во все орудие, требуется:

  • Java 8 или больше

  • Apache Maven 3.0.3 или больше

  • Jackson 2.4.5 или больше

Что бы внедрить его в проект, достаточно добавить две зависимости:

<dependency>    <groupId>io.swagger.core.v3</groupId>    <artifactId>swagger-annotations</artifactId>    <version>2.1.6</version></dependency><dependency>    <groupId>org.springdoc</groupId>    <artifactId>springdoc-openapi-ui</artifactId>    <version>1.5.2</version></dependency>

Также можно настроить maven плагин, что бы наша документация при сборке проект генерировалсь в YAML

<plugin> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-maven-plugin</artifactId> <version>0.3</version> <executions>   <execution>   <phase>integration-test</phase>   <goals>     <goal>generate</goal>   </goals>   </execution> </executions> <configuration>   <apiDocsUrl>http://localhost:8080/v3/api-docs</apiDocsUrl>   <outputFileName>openapi.yaml</outputFileName>   <outputDir>${project.build.directory}</outputDir> </configuration></plugin>

Дальше нам необходимо добавить конфиг в проект.

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

    @Bean    public GroupedOpenApi publicUserApi() {        return GroupedOpenApi.builder()                             .group("Users")                             .pathsToMatch("/users/**")                             .build();    }    @Bean    public OpenAPI customOpenApi(@Value("${application-description}")String appDescription,                                 @Value("${application-version}")String appVersion) {        return new OpenAPI().info(new Info().title("Application API")                                            .version(appVersion)                                            .description(appDescription)                                            .license(new License().name("Apache 2.0")                                                                  .url("http://springdoc.org"))                                            .contact(new Contact().name("username")                                                                  .email("test@gmail.com")))                            .servers(List.of(new Server().url("http://localhost:8080")                                                         .description("Dev service"),                                             new Server().url("http://localhost:8082")                                                         .description("Beta service")));    }

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

Вот некоторые из них:

  • @Operation - Описывает операцию или обычно метод HTTP для определенного пути.

  • @Parameter - Представляет один параметр в операции OpenAPI.

  • @RequestBody - Представляет тело запроса в операции

  • @ApiResponse - Представляет ответ в операции

  • @Tag - Представляет теги для операции или определения OpenAPI.

  • @Server - Представляет серверы для операции или для определения OpenAPI.

  • @Callback - Описывает набор запросов

  • @Link - Представляет возможную ссылку времени разработки для ответа.

  • @Schema - Позволяет определять входные и выходные данные.

  • @ArraySchema - Позволяет определять входные и выходные данные для типов массивов.

  • @Content - Предоставляет схему и примеры для определенного типа мультимедиа.

  • @Hidden - Скрывает ресурс, операцию или свойство

Примеры использования:

@Tag(name = "User", description = "The User API")@RestControllerpublic class UserController {}
    @Operation(summary = "Gets all users", tags = "user")    @ApiResponses(value = {            @ApiResponse(                    responseCode = "200",                    description = "Found the users",                    content = {                            @Content(                                    mediaType = "application/json",                                    array = @ArraySchema(schema = @Schema(implementation = UserApi.class)))                    })    })    @GetMapping("/users")    public List<UserApi> getUsers()

Swagger Codegen

Swagger Codegen - это проект, который позволяет автоматически создавать клиентские библиотеки API (создание SDK), заглушки сервера и документацию с учетом спецификации OpenAPI.

В настоящее время поддерживаются следующие языки / фреймворки:

  • API clients:

    • Java (Jersey1.x, Jersey2.x, OkHttp, Retrofit1.x, Retrofit2.x, Feign, RestTemplate, RESTEasy, Vertx, Google API Client Library for Java, Rest-assured)

    • Kotlin

    • Scala (akka, http4s, swagger-async-httpclient)

    • Groovy

    • Node.js (ES5, ES6, AngularJS with Google Closure Compiler annotations)

    • Haskell (http-client, Servant)

    • C# (.net 2.0, 3.5 or later)

    • C++ (cpprest, Qt5, Tizen)

    • Bash

  • Server stub:

    • Java (MSF4J, Spring, Undertow, JAX-RS: CDI, CXF, Inflector, RestEasy, Play Framework, PKMST)

    • Kotlin

    • C# (ASP.NET Core, NancyFx)

    • C++ (Pistache, Restbed)

    • Haskell (Servant)

    • PHP (Lumen, Slim, Silex, Symfony, Zend Expressive)

    • Python (Flask)

    • NodeJS

    • Ruby (Sinatra, Rails5)

    • Rust (rust-server)

    • Scala (Finch, Lagom, Scalatra)

  • API documentation generators:

    • HTML

    • Confluence Wiki

  • Other:

    • JMeter

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

<dependency>    <groupId>io.swagger</groupId>    <artifactId>swagger-codegen-maven-plugin</artifactId>    <version>2.4.18</version></dependency>

и если используете OpenApi 3.0, то:

<dependency>    <groupId>io.swagger.codegen.v3</groupId>    <artifactId>swagger-codegen-maven-plugin</artifactId>    <version>3.0.24</version></dependency>

Можно настроить maven плагин, и уже на процессе сборки мы можем сгенерировать нужный для нас клиент либо мок сервиса.

      <plugin>        <groupId>org.openapitools</groupId>        <artifactId>openapi-generator-maven-plugin</artifactId>        <version>3.3.4</version>        <executions>          <execution>            <phase>compile</phase>            <goals>              <goal>generate</goal>            </goals>            <configuration>              <generatorName>spring</generatorName>              <inputSpec>${project.basedir}/src/main/resources/api.yaml</inputSpec>              <output>${project.build.directory}/generated-sources</output>              <apiPackage>com.api</apiPackage>              <modelPackage>com.model</modelPackage>              <supportingFilesToGenerate>                ApiUtil.java              </supportingFilesToGenerate>              <configOptions>                <groupId>${project.groupId}</groupId>                <artifactId>${project.artifactId}</artifactId>                <artifactVersion>${project.version}</artifactVersion>                <delegatePattern>true</delegatePattern>                <sourceFolder>swagger</sourceFolder>                <library>spring-mvc</library>                <interfaceOnly>true</interfaceOnly>                <useBeanValidation>true</useBeanValidation>                <dateLibrary>java8</dateLibrary>                <java8>true</java8>              </configOptions>              <ignoreFileOverride>${project.basedir}/.openapi-generator-ignore</ignoreFileOverride>            </configuration>          </execution>        </executions>      </plugin>

Также все это можно выполнить с помощью командной строки.

Запустив джарник codegen и задав команду help можно увидеть команды, которые предоставляет нам Swagger Codegen:

  • config-help - Справка по настройке для выбранного языка

  • generate - Сгенерировать код с указанным генератором

  • help - Отображение справочной информации об openapi-generator

  • list - Перечисляет доступные генераторы

  • meta - Генератор для создания нового набора шаблонов и конфигурации для Codegen. Вывод будет основан на указанном вами языке и будет включать шаблоны по умолчанию.

  • validate - Проверить спецификацию

  • version - Показать информацию о версии, используемую в инструментах

Для нас самые нужные команды это validate, которая быстро проверять на валидность спецификации и generate,с помощью которой мы можем сгенерировать Client на языке Java

  • java -jar openapi-generator-cli-4.3.1.jar validate -i openapi.yaml

  • java -jar openapi-generator-cli-4.3.1.jar generate -i openapi.yaml -g java --library jersey2 -o client-gener-new

Swagger UI

Swagger UI - позволяет визуализировать ресурсы API и взаимодействовать с ними без какой-либо логики реализации. Он автоматически генерируется из вашей спецификации OpenAPI (ранее известной как Swagger), а визуальная документация упрощает внутреннюю реализацию и использование на стороне клиента.

Вот пример Swagger UI который визуализирует документацию для моего pet-project:

Нажавши на кнопку "Try it out", мы можем выполнить запрос за сервер и получить ответ от него:

Swagger Editor

Swagger Editor - позволяет редактировать спецификации Swagger API в YAML внутри вашего браузера и просматривать документацию в режиме реального времени. Затем можно сгенерировать допустимые описания Swagger JSON и использовать их с полным набором инструментов Swagger (генерация кода, документация и т. Д.).

На верхнем уровне в спецификации OpenAPI 3.0 существует восемь объектов. Внутри этих верхнеуровневых объектов есть много вложенных объектов, но на верхнем уровне есть только следующие объекты:

  1. openapi

  2. info

  3. servers

  4. paths

  5. components

  6. security

  7. tags

  8. externalDocs

Для работы над документацией со спецификацией используется онлайн-редактор Swagger Редактор Swagger имеет разделенное представление: слева пишем код спецификации, а справа видим полнофункциональный дисплей Swagger UI. Можно даже отправлять запросы из интерфейса Swagger в этом редакторе.

Редактор Swagger проверит контент в режиме реального времени, и укажет ошибки валидации, во время кодирования документа спецификации. Не стоит беспокоиться об ошибках, если отсутствуют X-метки в коде, над которым идет работа.

Первым и важным свойством для документации это openapi. В объекте указывается версия спецификации OpenAPI. Для Swagger спецификации это свойство будет swagger:

 openapi: "3.0.2"

Объект info содержит основную информацию о вашем API,включая заголовок, описание, версию, ссылку на лицензию, ссылку на обслуживания и контактную информацию. Многие из этих свойство являются не обязательными.

info:  title: "OpenWeatherMap API"  description: "Get the current weather, daily forecast for 16 days, and a three-hour-interval forecast for 5 days for your city."  version: "2.5"  termsOfService: "https://openweathermap.org/terms"  contact:  name: "OpenWeatherMap API"    url: "https://openweathermap.org/api"    email: "some_email@gmail.com"  license:    name: "CC Attribution-ShareAlike 4.0 (CC BY-SA 4.0)"    url: "https://openweathermap.org/price"

Объект servers указывает базовый путь, используемый в ваших запросах API. Базовый путь - это часть URL, которая находится перед конечной точкой. Объект servers обладает гибкой настройкой. Можно указать несколько URL-адресов:

servers:  - url: https://api.openweathermap.org/data/2.5/        description: Production server  - url: http://beta.api.openweathermap.org/data/2.5/        description: Beta server  - url: http://some-other.api.openweathermap.org/data/2.5/        description: Some other server

paths - Это та же конечная точка в соответствии с терминологии спецификации OpenAPI. Каждый объект path содержит объект operations - это методы GET, POST, PUT, DELETE:

paths:  /weather:     get:

Объект components уникален среди других объектов в спецификации OpenAPI. В components хранятся переиспользуемые определения, которые могут появляться в нескольких местах в документе спецификации. В нашем сценарии документации API мы будем хранить детали для объектов parameters и responses в объекте components

Conclusions

  • Документация стала более понятней для бизнес юзера так и для техническим юзерам (Swagger UI, Open Specifiation)

  • Может генерировать код для Java, PHP, .NET, JavaScrypt (Swager Editor, Swagger Codegen)

  • Можно проверять насколько совместимы изменения. Можно настраивать это в дженкинсе

  • Нет ни какой лишней документации к коде, код отдельно, документация отдельно

Подробнее..
Категории: Java , Api , Spring , Swagger , Openapi

Документируй это

08.05.2021 12:05:15 | Автор: admin

Всем привет! В данной статье хотел бы рассмотреть инструменты документирования в принципиально разных подходах в разработке REST API, а именно для CodeFirst - инструменты SpringRestDocs (а также его надстройку SpringAutoRestDocs) и для ApiFirst - инструменты экосистемы Swagger(Open-Api).

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

CodeFirst плюс SpringAutoRestDocs

Как уже описывали в статье про SpringRestDocs это инструмент достаточно широкого использования, позволяющий генерировать различную документацию (аскедок, хтмл и т.д.) на основе тестов. Пожалуй один из немногих недостатков этого инструмента, является его многословность в тестах, а именно - необходимо описывать каждый параметр, каждое поле и т.д. В свою очередь тесы в SpringAutoRestDocs, используя JSR и Spring аннотации, а также Javadoc, становятся более коротким и немногословными.

Чуть более подробно про SpringAutoRestDocs

SpringAutoRestDocs это надстройка над SpringRestDocs, которая ставит своей целью уменьшение написания кода и документации. Основное преимущество этого иструмента - является меньшее количество написанной документации и большая связаность с рабочим кодом.

Некоторые из фичей инструмента:

  • Автоматическое документирования полей запроса и ответа, хедеров и параметров запроса с использованием Jackson, а также описательной части на основе Javadocs

  • Автоматическое документирование опциональности и ограничения полей по спецификации JSR-303

  • Возможность кастомизации итоговых снипеттов

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

Для демонстрации возьмем классический Swagger PetStore (с небольшой модернизацией, подробно можно посмотреть спеку в репозитории) и имплементируем несколько методов контроллера (addPet, deletePet, getPetById). В статье будет приведен пример на основе одного метода getPetById

Контроллер:

@RestController@RequiredArgsConstructorpublic class PetController implements PetApi {    private final PetRepository petRepository;        @Override    public ResponseEntity<Pet> getPetById(Long petId) {        return new ResponseEntity<>(petRepository.getPetById(petId), HttpStatus.OK);    }  //и другие имплементированные методы}

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

Базовые настройки для тестовой автодокументации:

Для начала нам необходимо определить базовый класс для всех тестов, который опишет основные правила нашей автоматической документации. Основное что нам понадобится, это сконфигурированный MockMvc (более подробно можно посмотреть в репозитории):

@Beforevoid setUp() throws Exception {  this.mockMvc = MockMvcBuilders    .webAppContextSetup(context)    .alwaysDo(prepareJackson(objectMapper, new TypeMapping()))    .alwaysDo(commonDocumentation())    .apply(documentationConfiguration(restDocumentation)           .uris().withScheme("http").withHost("localhost").withPort(8080)           .and()           .snippets().withTemplateFormat(TemplateFormats.asciidoctor())           .withDefaults(curlRequest(), httpRequest(), httpResponse(),                         requestFields(), responseFields(), pathParameters(),                         requestParameters(), description(), methodAndPath(),                         section(), links(), embedded(), authorization(DEFAULT_AUTHORIZATION),                         modelAttribute(requestMappingHandlerAdapter.getArgumentResolvers())))    .build()  }protected RestDocumentationResultHandler commonDocumentation(Snippet... snippets) {  return document("rest-auto-documentation/{class-name}/{method-name}", commonResponsePreprocessor(), snippets)  }protected OperationResponsePreprocessor commonResponsePreprocessor() {  return preprocessResponse(replaceBinaryContent(), limitJsonArrayLength(objectMapper), prettyPrint())  }
Чуть более подробно про настройки MockMVC

WebApplicationContext получаем от @SpringBootTest

prepareJackson(objectMapper, new TypeMapping()) позволяет настроить ResultHandler, который подготовит наши pojo для библиотеки документирования

withDefaults(curlRequest(), httpRequest(), и т.д.) набор сниппетов, которые будут сгенерированы

commonDocumentation() описывает директорию, куда в build папке разместятся сгенерированные сниппеты, а также препроцессинг ответа контроллера.

Непосредственно сам тест:

Пример теста более подробно можно посмотреть в репозитории.

@Testvoid getPetTest() {//givendef petId = 1L//whendef resultActions = mockMvc.perform(RestDocumentationRequestBuilders.get("/pet/{petId}", petId).header("Accept", "application/json"))//thenresultActions.andExpect(status().isOk()).andExpect(content().string(objectMapper.writeValueAsString(buildReturnPet())))}

Здесь приведен стандартный MockMvc тест, с ожидаемым статусом и ожидаемым телом ответа.

Итого на выходе:

Набор снипеттов с "главным" сниппетом под названием auto-section.adoc (который содержит информацию из всех остальных, указанных в настройках MockMVC) из которых позже можно собрать общий index.adoc для всех методов API. Готовая структура сниппетов:

Готовые сниппеты документы можно посмотреть в репозитории: сниппеты, html сгенеренный на основе сниппетов.

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

Чуть подробнее про кастомизацию сниппетов и ограничений

Функционал кастомизации в SpringAutoRestDocs базируется на таковом же в SpringRestDocs.

К примеру русификация шапок сниппетов и описание ограничений:

ApiFirst плюс Swagger

Как уже описывалось во множестве статей (пример1, пример2) swagger - это open-source проект, включающий OpenApi Specification и широкий набор инструментов для описания, визуализации и документирования REST api.

Чуть более подробно про Swagger

Swagger состоит из двух основных частей - это OpenApi Specification и Swagger Tools.

  • OpenApi Specification - это спецификация описывающая процесс создания REST контракта для более простого процесса разработки и внедрения API. Спецификация может быть описана в формате YAML и JSON. Описание базового синтаксиса, а также более подробную информацию можно изучить по ссылке.

  • Swagger Tools - это инструменты визуализации, документации и генерации клиентно-серверной части REST api. Состоят из трех основных блоков: Swagger Editor(браузерный эдитор, для более удобного написания контракта с поддержкой валидации ситаксиса), Swagger UI(рендер спецификации в качестве интерактивной документации API), Swagger Codegen(генератор серверно-клиентной части, а также некоторых типов документаций)

Ниже мы рассмотрим мульти-модульный проект со следущей структурой: библиотека со свагер генератором (swagger-library), стартер с swagger-ui(swagger-webjar-ui-starter), приложение которое имплементирует классы библиотеки(spring-auto-rest-docs).

Для демонстрации возможностей Swagger возьмем классический Swagger PetStore из примера SpringAutoRestDocs выше.

Настройки build.gradle для модуля библиотеки:

Для реализации генерации server stub и документации на основе Swagger контракта используем OpenApi Generator.

Чуть более подробно про OpenApi Generator

В модуле используется gradle плюс gradle plugin OpenApi Generator.

Сам плагин дает возможность генерить как серверно-клиентную часть, так и различные форматы документации. Более подробно можно посмотреть в ссылке на репозиторий.

Основная таска для генерации библиотеки и ее настройки:

openApiGenerate {    generatorName = 'spring'    inputSpec = specFile    outputDir = "${project.projectDir}/"    id = "${artifactId}"    groupId = projectPackage    ignoreFileOverride = ignoreFile    apiPackage = "${projectPackage}.rest.api"    invokerPackage = "${projectPackage}.rest.invoker"    modelPackage = "${projectPackage}.rest.model"    configOptions = [            dateLibrary            : 'java8',            hideGenerationTimestamp: 'true',            interfaceOnly          : 'true',            delegatePattern        : 'false',            configPackage          : "${projectPackage}.configuration"    ]}
Чуть более подробно про настройки таски openApiGenerate

Незабываем выполнять генерацию кода до компиляции проекта:

task codegen(dependsOn: ['openApiGenerate', 'copySpecs'])compileJava.dependsOn(codegen)compileJava.mustRunAfter(codegen)

Копируем спеки в ресурсы, чтобы была позже возможность отобразить UI прямо из спек:

task copySpecs(type: Copy) {    from("${project.projectDir}/specs")    into("${project.projectDir}/src/main/resources/META-INF/specs")}

При необходимости также можно сгенерить Asciidoc или Html2.

Swagger-ui стартер:

Стартер добавляет в регистр ресурсов спеки с определенными в дефолтными настройками и webjar swagger-ui с измененным путем до дефолтного контракта.

Rest-docs API:

В самом приложении достаточно имплементировать сгенерированный интерфейс и переопределить настройки UI стартера:

@RestController@RestController@RequiredArgsConstructorpublic class PetController implements PetApi {    private final PetRepository petRepository;        @Override    public ResponseEntity<Pet> getPetById(Long petId) {        return new ResponseEntity<>(petRepository.getPetById(petId), HttpStatus.OK);    }  //и другие имплементированные методы}
swagger:  ui:    indexHandler:      enabled: true      resourceHandler: "/api/**"    apis:      - url: http://localhost:8080/specs/some_swagger.yaml        name: My api

Итого на выходе:

Server stub - готовая библиотека с сущностями и интерфейсом для реализации серверной части API.

Swagger UI - с помощью которого мы получаем наглядную визуализацию API и возможность направлять запросы прямо из UI:

Также примеры Asciidoc или Html2 из самого swagger контракта:

Заключение:

Что дает SpringAutoRestDocs:

  • Возможность хранить документацию как в проекте (к примеру в форме собранного index.html из множества сниппетов), так и в любом другом удобном месте.

  • Документация и модель данных всегда синхронизирована с кодом, так как документация генерируется на основе "зеленых" тестов контроллера.

  • Возможность кастомизации сниппетов и ограничений.

Что дает Swagger:

  • Единый артефакт на всех этапах разработки.

  • Документация и модель данных всегда синхронизирована с кодом, так как код генерируется на основе контракта.

  • Возможность использовать UI, в котором будет вся та же информация, что в контракте с возьможностью направлять запросы.

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

Подробнее..
Категории: Java , Api , Spring , Documentation , Swagger , Spring auto rest docs

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru