Хабрахабр

[Из песочницы] Система автоматического документирования REST-API в Laravel проектах

Преамбула

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

  1. Описывать своим коллегам правила обращения к серверу на пальцах
    Этот метод быстр и не требует долгосрочной поддержки, но высока вероятность, что вас за это будут бить.
  2. Руками составлять Google-docs/Wiki/Readme в проекте
    Удобно тем, что однажды написанная документация не требует повторного объяснения. Её можно показать коллегам и даже иногда заказчику. Минусом данного метода является долгосрочная поддержка такой документации. Когда Api в проекте вырастает до таких размеров, что сама мысль "А когда же я обновлял документацию?" вызывает холодок по спине, тогда вы понимаете, что дальше так продолжаться не может. Формально вы можете обновлять документацию очень часто и маленькими фиксами, но это до первого отпуска.
  3. Основная идея заключается в том, что к проекту пристыковывается некий плагин, который собирает информацию по вашему коду, сам составляет документацию и обёртывает её в удобочитаемый формат. Использовать систему автодокументирования
    И вот для того, чтобы решить минусы первых двух методов человечество придумало системы автоматического документирования. Давайте попробуем сделать инструмент, который поможет получить документацию нашего проекта с минимальным количеством телодвижений Но большинство решений по этому методу не идеальны.

    Проблема

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

    • При большом объёме кода легко допустить ошибки даже в такой простой задаче. Добавление аннотаций в уже написанный проект.
      Добавление аннотаций для всех методов всех контроллеров уже реализованного проекта – задача довольно рутинная.

    • Так что вам неизбежно придётся следить за актуальность всех комментариев разбросанных по проекту. Поддержка аннотаций.
      В действительности аннотации не решают этой проблемы, они лишь объединяют её с поддержкой кода.

    • Захламление кода
      Для демонстрации обратимся к классическому примеру от системы Swagger и посмотрим как устроен контроллер

      /**
      * @SWG\Get(
      * path="/pet/",
      * summary="Find pet by ID",
      * description="Returns a single pet",
      * operationId="getPetById",
      * tags={"pet"},
      * consumes={
      * "application/xml",
      * "application/json",
      * "application/x-www-form-urlencoded"
      * },
      * produces={"application/xml", "application/json"},
      * @SWG\Parameter(
      * description="ID of pet to return",
      * in="path",
      * name="petId",
      * required=true,
      * type="integer",
      * format="int64"
      * ),
      * @SWG\Response(
      * response=200,
      * description="successful operation",
      * @SWG\Schema(ref="#/definitions/Pet")
      * ),
      * @SWG\Response(
      * response="400",
      * description="Invalid ID supplied"
      * ),
      * @SWG\Response(
      * response="404",
      * description="Pet not found"
      * ),
      * security={
      * {"api_key": {}},
      * {"petstore_auth": {"write:pets", "read:pets"}}
      * }
      * )
      */
      public function doSomethingSmart()
      { return failItAllAndReturn500Response();
      }

      Соотношение комментариев к коду будет удручающее. А теперь представьте, что у вас в контроллере 5 методов, каждый из которых по 10 строчек максимум?

    • Актуальность документации
      Данный пример демонстрирует еще один недостаток — документация не зависит от того как реально работает ваш код. Несмотря на то, что данный метод всегда будет возвращать ответ с кодом 500, в документации будут значиться ответы 200, 400, 404.

Решение

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

Реализация

Для каждого из роутов вы можете конфигурировать свой список посредников. Весь принцип действия основан на паттерне Middleware, то есть посредник. Каждый из запросов прежде чем попасть в контроллер пройдёт через цепочку посредников, каждый из которых может сделать нечто умное(или не очень).

public function handle($request, Closure $next) { $response = $next($request); if ((config('app.env') == 'testing')) { $this->service->addData($request, $response); } return $response; }

В сервисе происходит сбор необходимой информации из запроса и ответа. По этому коду видно, что плагин начинает сбор информации во время тестирования. Единственная сложность состоит в получении правил валидации. Request нам возвращает URI и хедеры, а так же роут, по которому производится текущее действие. Поэтому рекомендуется для валидации "инжектить" к методам контроллера классы реквестов.
Например, вот так В middleware приходит экземпляр класса Illuminate\Http\Request, из которого получить данные о валидации невозможно.

public function someAction(MyAwersomeRequest $request) { ..... }

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

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

Применение

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

/app/Http/Controllers/TestController.php

class TestController extends Controller
{ public function lists() { $data = [ 'some' => 'complex', 'structure' => [ 'with' => 'multiple', 'nesting' => [] ] ]; return response()->json($data); } ...
}

Далее требуется прописать роут, по которому будет вызываться метод lists нашего контроллера.
/routes/api.php Как вы можете заметить этот метод просто возвращает json объект.

...
Route::get('/test', ['uses' => TestController::class . '@lists']);

И соответственно применить middleware AutoDoc-плагина
/app/Http/Kernel.php

protected $middlewareGroups = [ 'api' => [ ... AutoDocMiddleware::class ], ];

Чтоб его протестировать давайте создадим следующий тест

class ExampleTest extends TestCase
{ public function testGetList() { $this->json('get', '/api/test'); $this->assertResponseOk(); }
}

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

public function tearDown() { $currentTestCount = $this->getTestResultObject()->count(); $allTestCount = $this->getTestResultObject()->topTestSuite()->count(); if (($currentTestCount == $allTestCount) && (!$this->hasFailed())) { $autoDocService = app(SwaggerService::class); $autoDocService->saveProductionData(); } parent::tearDown(); }

После запуска тестов мы можем увидеть собранную документацию по тому роуту, который указали для документации в конфиге config/auto-doc.php

Выглядеть она будет примерно вот так:

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

php artisan make:request TestGetRequest

class TestGetRequest extends Request
{ /** * Determine if the user is authorized to make this request. * * @return bool */ public function authorize() { return true; } /** * Get the validation rules that apply to the request. * * @return array */ public function rules() { return [ 'not-found' => 'boolean', 'need-authorization' => 'boolean', 'test-parameter' => 'integer' ]; }
}

Тут специально присутствуют параметры not-found, need-authorization, test-parameter, чтобы промоделировать разные ответы.
Для того, чтобы это работало как ожидается давайте добавим в метод нашего контроллера пару проверок

public function lists(TestGetRequest $request) { if ($request->input('not-found')) { return response()->json([ 'error' => 'entity not found' ], Response::HTTP_NOT_FOUND); } if ($request->input('need-authorization')) { return response()->json([ 'error' => 'authorization failed' ], Response::HTTP_UNAUTHORIZED); } return response()->json([ 'some' => 'complex', 'structure' => [ 'with' => 'multiple', 'nesting' => [] ] ]); }

Осталось дело за малым — давайте добавим еще три теста!

public function testGetListNotFound() { $response = $this->json('get', '/api/test', [ 'not-found' => true ]); $response->assertStatus(Response::HTTP_NOT_FOUND); } public function testGetListNoAuth() { $response = $this->json('get', '/api/test', [ 'need-authorization' => true ]); $response->assertStatus(Response::HTTP_UNAUTHORIZED); } public function testGetListWrongParameters() { $response = $this->json('get', '/api/test', [ 'test-parameter' => 'test' ]); $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); }

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


Например, отсутствие детального описания нашего запроса. Но чего тут не хватает? Так же тут используются стандартные описания ответов, а хотелось бы иногда конкретизировать что именно подразумевается под каким-либо кодом ответа и входным параметром. Краткое описание(test get request) — это переформатированное имя класса TestGetRequest. Короче хочется
играться.
Давайте добавим аннотации к классу testGetRequest

/** * @description * This request designed to demonstrate request with full annotation and response witch contain some data. * It has multi-line description witch will be displayed in Annotation Notes block of documentation. * It has custom summary, response code descriptions and parameters descriptions. * * @summary Test Get Request with full annotations * * @_204 This request has successfully done * @_401 You must remove need-authorization flag from input parameters for pass authorization. * @_404 We so sorry but you entity not exists. * @_422 Wrong input parameter. It must be integer * * @need-authorization If this parameter is true then you will get 401 response * @not-found If this parameter is true then you will get 404 response * @test-parameter This parameter designed for demonstrate unprocesable entity response */ class TestGetRequest extends Request {...}

Ни один из параметров в аннотации не является обязательным.



Делается это в файле config/auto-doc.php. Так же есть возможность устанавливать стандартное описание кода ответа на уровне приложения. Приоритет описаний следующий:

  • описание кода ответа в аннотации
  • значение из конфига
  • стандартное описание кода ответа

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

php artisan vendor:publish

Например, если добавить туда следующий код В папку resources/views помещается файл swagger-description.blade.php.

This project designed to demonstrate working of <b>ronasit/laravel-swagger</b> plugin. Here is project description from <b>swagger-description.blade.php</b>
<div class="swagger-ui"> <div class="opblock-body"> <pre> You can add some code here </pre> </div>
</div>
Or some image
<div style="display: flex; justify-content: center; width: 100%"> <img src="/img/hqdefault.jpg"/>
</div>

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

Итог

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

Ссылки

Репозиторий этого плагина находится вот тут

Демонстрационный проект тут

Теги
Показать больше

Похожие статьи

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

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть