Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jettary/f1430caa2c74b405bc1496104ebd5694 to your computer and use it in GitHub Desktop.
Save jettary/f1430caa2c74b405bc1496104ebd5694 to your computer and use it in GitHub Desktop.

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

Примером хорошей документации, которая упрощает жизнь разработчикам бэкенда, фронтенда и особенно тестировщикам - Swagger: описание входных и выходных моделей, параметры запросов, переключение окружение, наследование и расширение моделей. Но как говорил Бенджен Старк "Я научился не слышать всё, что идёт до слова НО", и это НО - комбинация Swagger и node.js

Есть несколько подходов к реализации связки Node.js + Swagger. Первый из них - использовать модуль swagger-node. Работа с ним крайне проста:

$> npm install -g swagger
$> swagger project create hello-world
$> swagger project edit
… some magic and controllers editing…
$> swagger project start

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

Второй способ, который более совместим с крупными проектами - Swagger UI. Он может быть может быть использован в качестве модуля в уже существующем приложении, или же запущен в собственном Docker контейнере:

$> npm install -g swagger-ui swagger-dist
// app.js
const express = require('express')
const pathToSwaggerUi = require('swagger-ui').absolutePath()
const app = express()
app.use(express.static(pathToSwaggerUi))
app.listen(3000)

При использовании Docker контейнера можно воспользоваться готовым, запустив его с указанием расположения файла документации, но я хочу применить подход, который позволяет произвести более тонкую настройку. Стоит начать с создания Docker контейнера, в который нужно включить статичный Swagger UI и nginx. Для окончания завершения настройки связки Приложения + Swagger остается сделать 2 вещи: уточнить ендпоинт для загрузки и подготовить саму документацию. Если первая операция достаточно простая, то вторая требует единый файл конфигурации.

Недостаток сваггера в ”чистом” виде - отсутствие поддержки наследования моделей и сложный и не оптимальный механизм разбиения на модули. Модульность - единственный способ организации кода и документации в проектах больше чем “Hello World”.

Проблему модульности можно решить двумя путями: использование сторонних библиотек и написанием собственных решений. Самая популярная библиотека дает возможность использовать встроенный механизм ссылок $ref: ‘../path/to/some/file’ что, несомненно, удобно, но ограничивает в ссылках на описание модели внутри этого же файла.

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

paths:
	/api/login/:
	/api/user/:
	/api/some-model/:
definitions:
	LoginResponseModel:
	LoginPayloadModel:
	UserModel:

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

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

const _ = require('lodash');
const fs = require('fs');
const yaml = require('js-yaml');
const header = {
    swagger: '2.0',
    info: {
        version: '1.0.0',
        title: 'Some Title',
        description: 'Some funny description'
    },
    schemes: ['http', 'https'],
    basePath: '/api'
};

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

/* object */ function readDocs(dir /* string */) {
    const files = fs.readdirSync(dir);
    const docs = {};

    files.forEach(file => {
        if (! /\.yaml$/.test(file)) {
            /* do nothing */
            return;
        }
        const fullpath = path.join(dir, file)

        const data = yaml.load(fs.readFileSync(fullpath, 'utf-8').toString());
        addGeneratedFields(data);
        _.merge(docs, data);
    });

    return docs;
}

Функция addGeneratedFields, упомянутая выше добавляет ко всем объвленным эндпоинтам документации обязательные поля, такие как заголовок Authorization, переменные для доступа к элементам ваших коллекций. В перспективе вы сохраните на этом немало времени и строк кода, ведь не придётся копировать из одного места в другое, а каждый такой параметр будет описан единообразно. А добавление новых, обязательных для всех параметров станет проще и быстрее.

/* void */ function addGeneratedFields(data /* object */) {
    const authParam = { 
      in: 'header',
      name: 'Authorization',
      description: 'Authorization token using bearer schema',
      required: true, type: 'string'
    };
    const pathRequiredParam = (name /* string */) => ({
      in: 'path',
      name: name,
      required: true,
      type: 'string',
      description: 'Path parameter'
    });

    if (! ('paths' in data)) {
        return;
    }

    for (const path in data.paths) {
        for (const method in data.paths[path]) {
            const parameters = data.paths[path][method].parameters || [];

            const pathParams = path.match(/{([\w\s\|]+)}/gi);
            if (pathParams) {
                /* path params exists */
                for (const pathParam of pathParams.reverse()) {
                    const paramName = pathParam.replace('{', '').replace('}', '')
                    parameters.unshift(pathRequiredParam(paramName));
                }
            }

            parameters.unshift(authParam);
            data.paths[path][method].parameters = parameters;
        }
    }
}

И последняя необходимая часть - сохранение всего это в файл. Для этого лучше воспользоваться механизмом fs.createWriteStream и stream.write из ядра Node.js.

module.exports = function GenerateDocs(dir /* string */, outFile /* string */) {
    const stream = fs.createWriteStream(outFile, { encoding: 'utf-8', flags: 'w' });

    stream.once('open', fd => {
        stream.write('# This file is generated\n');
        stream.write(yaml.dump(header));
        stream.write(yaml.dump(readDocs(dir)));
        stream.end();
    });
}

Менее 100 строк добавляют модульности вашей документации. Конечно их можно расширять и улучшать:

  • добавить разбиение на поддиректории
  • добавить к имени модели название файла, если документация очень большая и вы боитесь пересечения имён
  • реализовать механизм наследования моделей Swagger $allOf, $oneOf, $anyOf
  • limit / offset / page для списковых роутов

Посмотрев на результат, вы бы могли подумать, что это изобретение велосипеда. И - да, и - нет. Наработки, вроде этой, помогают произвести более тонкую настройку написания документации принятой в команде (и в вашем случае процесс может быть совсем другим). Можно сказать, что это необходимое зло, но которое упрощает и делает приятнее процесс написания документации. А со временем "локальный велосипед" можем перерости в более гибку библиотеку.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment