@octokit/rest изначально не является оригинальной разработкой GitHub, и представляет собой адаптацию github — самого популярного пакета 2017 года от пользователя @bkeepers. В этом посте будем говорить про @octokit/rest — теперь официальный JavaScript SDK для GitHub REST API.


Грегор (автор статьи) является разработчиком JavaScript Octokit. Он бывалый open source разработчик, с особой тягой к автоматизации задач и снижению порога вхождения для контрибьюторов всех видов и профессий. Помимо Octokit, Грегор работает над Probot, nock и semantic-release. В свободное время он заботится о своих тройняшках Нико, Аде и Киане. Больше материалов Грегора можно найти на DEV community и Twitter.

legacy


Позже переименованный в @octokit/rest, пакет github был одним из старейших проектов в экосистеме Node. Первый коммит сделан в июне 2010 года. Это были времена Node v0.1, когда package.json еще не существовал, а реестр npm был всё ещё в разработке.


В 2017 году GitHub наняли меня, чтобы переработать пакет github в официальный GitHub API JavaScript SDK для браузеров и Node.js. Здесь можно найти мой первый коммит в сентябре 2017 года. На тот момент в проекте было около 16 тысяч строк кода, разбитых на три JavaScript файла, один огромный JSON и два файла для определений типов TypeScript/Flow.


?  rest.js git:(50720c8) wc -l lib/*
     120 lib/error.js
    3246 lib/index.d.ts
     905 lib/index.js
    3232 lib/index.js.flow
      17 lib/promise.js
    7995 lib/routes.json
     143 lib/util.js
   15658 total

Разработка


Первой основной целью проекта была поддерживаемость кода. Тогда ключевым компонентом библиотеки был огромный файл routes.json на почти 8 тысяч строк, который определял все конечные точки REST API GitHub. Его поддержка осуществлялась вручную, а создание/изменение роутов было следствием случайного обнаружения проблемы.


Учитывая этот факт, я написал скрипт (octokit/routes) для автоматического анализа документации REST API GitHub и вывода результата в JSON. Это решило проблему поддержки routes.json. Если скрипт обнаруживал изменения, @octokit/rest получал PR с обновлениями файла routes.json, и после мерджа происходил автоматический релиз. Благодаря такой автоматизации, файл routes.json теперь гарантированно покрывал все конечные точки REST API GitHub и состоял из 10 275 строк кода. Сопутствующие определения типов TypeScript увеличились до более чем 26 700 строк кода.


Архитектура


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


Прим. переводчика: В оригинальной статье используется термин "decomposability", который дословно можно перевести как "разложимость", что не совсем удачно передает смысл. Здесь имеется ввиду "разделение на части", что можно объяснить как "разделение на модули" или "модульность".

JavaScript Octokit предназначен для всех сред выполнения JavaScript, некоторые из которых имеют строгие ограничения. Например, размер пакета является критическим показателем при использовании в браузере. Поэтому, вместо единой монолитной библиотеки, которая содержит полный REST API, стратегии аутентификации и рекомендованный вспомогательный функционал (например "пагинация"), важно предоставить пользователям доступ к более низкому уровню. Таким образом, будет возможен компромисс между размером пакета и предоставляемым функционалом.


Вот обзор архитектуры, которую я разработал в январе 2018 года:



Результат внутреннего рефакторинга до новой архитектуры:
Обратите внимание, что этот пример был упрощен для удобства чтения


?  rest.js git:(f7c9f86) wc -l index.* lib/**/*.{js,json}
      31 index.js
    3474 index.d.ts
    3441 index.js.flow

     101 lib/endpoint/ # 4 files
     162 lib/request/ # 3 files

      83 lib/plugins/authentication/ # 3 files
     130 lib/plugins/endpoint-methods/ # 4 files
     130 lib/plugins/pagination/ # 11 files

      58 lib/parse-client-options.js
   10628 lib/routes.json

   18238 total

В течение следующих шести месяцев я реорганизовал код и начал извлекать некоторые из модулей:


  • @octokit/endpoint: превращает параметры конечной точки API REST в общие параметры http-запроса
  • @octokit/request: отправляет параметризованные запросы в API GitHub с разумными настройками по умолчанию в браузерах и Node
  • before-after-hook: API использующееся для подключения к жизненному циклу запроса

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


Новая архитектура внутреннего кода теперь выглядела так:
Обратите внимание, что этот пример был упрощен для удобства чтения


?  rest.js git:(01763bf) wc -l index.* plugins/**/*.{js,json} lib/**/*.js
      14 index.js
   26714 index.d.ts

     110 lib/ # 6 files

      86 plugins/authentication/ # 3 files
      77 plugins/pagination/ # 3 files
      39 plugins/register-endpoints/ # 3 files
     108 plugins/validate/ # 2 files
   10275 plugins/rest-api-endpoints/routes.json
   37423 total

Позже я создал @octokit/core — новую базовую библиотеку Octokit JavaScript, на которой будут основываться @octokit/rest и остальные библиотеки группы Octokit. Большая часть его логики была извлечена из @octokit/rest за исключением устаревших функций. Я не стал использовать его сразу в @octokit/rest, чтобы избежать критических изменений.


Поскольку @octokit/core был свободен от любого legacy и проблем с обратной совместимостью, я продолжил эксперименты с разбивкой средств аутентификации. Результатом стали отдельные пакеты для каждой стратегии аутентификации — все они перечислены в README @octokit/auth. Если вы хотите узнать больше о стратегиях аутентификации GitHub, то предлагаю ознакомиться с моей серией статей (eng).


@octokit/core и отдельные библиотеки аутентификации заменяют весь код из lib/* и plugins/authentication/*. Осталось только три плагина, которые я извлек в дальнейшем:



Плагин validate устарел благодаря типизации от TypeScript во время компиляции и больше не было необходимости проверять параметры запроса на клиенте. Это значительно уменьшило количество кода и зависимостей. Например, вот текущее определение для метода octokit.checks.create():


{
  checks: {
    create: {
      headers: { accept: "application/vnd.github.antiope-preview+json" },
      method: "POST",
      params: {
        actions: { type: "object[]" },
        "actions[].description": { required: true, type: "string" },
        "actions[].identifier": { required: true, type: "string" },
        "actions[].label": { required: true, type: "string" },
        completed_at: { type: "string" },
        conclusion: {
          enum: [
            "success",
            "failure",
            "neutral",
            "cancelled",
            "timed_out",
            "action_required"
          ],
          type: "string"
        },
        details_url: { type: "string" },
        external_id: { type: "string" },
        head_sha: { required: true, type: "string" },
        name: { required: true, type: "string" },
        output: { type: "object" },
        "output.annotations": { type: "object[]" },
        "output.annotations[].annotation_level": {
          enum: ["notice", "warning", "failure"],
          required: true,
          type: "string"
        },
        "output.annotations[].end_column": { type: "integer" },
        "output.annotations[].end_line": { required: true, type: "integer" },
        "output.annotations[].message": { required: true, type: "string" },
        "output.annotations[].path": { required: true, type: "string" },
        "output.annotations[].raw_details": { type: "string" },
        "output.annotations[].start_column": { type: "integer" },
        "output.annotations[].start_line": { required: true, type: "integer" },
        "output.annotations[].title": { type: "string" },
        "output.images": { type: "object[]" },
        "output.images[].alt": { required: true, type: "string" },
        "output.images[].caption": { type: "string" },
        "output.images[].image_url": { required: true, type: "string" },
        "output.summary": { required: true, type: "string" },
        "output.text": { type: "string" },
        "output.title": { required: true, type: "string" },
        owner: { required: true, type: "string" },
        repo: { required: true, type: "string" },
        started_at: { type: "string" },
        status: { enum: ["queued", "in_progress", "completed"], type: "string" }
      },
      url: "/repos/:owner/:repo/check-runs"
    }
  }
}

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


{
  checks: {
    create: [
      "POST /repos/{owner}/{repo}/check-runs",
      { mediaType: { previews: ["antiope"] } },
    ];
  }
}

В конце, ранее извлеченный код был собран в обещанные 10 строк:


import { Octokit as Core } from "@octokit/core";
import { requestLog } from "@octokit/plugin-request-log";
import { paginateRest } from "@octokit/plugin-paginate-rest";
import { restEndpointMethods } from "@octokit/plugin-rest-endpoint-methods";

import { VERSION } from "./version";

export const Octokit = Core.plugin([
  requestLog,
  paginateRest,
  restEndpointMethods,
]).defaults({ userAgent: `octokit-rest.js/${VERSION}` });

Тесты


Каждая строка кода была изменена между версиями v16 и v17, поэтому единственный способ убедиться в отсутствии новых ошибок — провести полное тестирование.


На момент создания модуля в 2017 году у нас не было никаких тестов, но были примеры использования. Первым делом я превратил их в интеграционные тесты, а поскольку JavaScript Octokit SDK задумывался как основа SDK для всех популярных языков, я создал octokit/fixtures — независимый от языка, автоматически обновляемый набор макетов http для общих случаев использования.


Для оставшейся логики, специфичной для @octokit/rest, были написаны интеграционные тесты для 100% покрытия. На сегодняшний момент, если значение опустится ниже 100% тесты упадут.


Прим. переводчика: да, это жёстко

Работая над переходом на v17 с 10 строками кода, я продолжал запускать тесты из v16, за исключением тестов для устаревшего API. В то же время, слишком большое количество тестов не слишком хорошо, поэтому после успешной проверки v17, тесты не относящиеся к @octokit/rest были удалены. Некоторые из них были перенесены в плагины, @octokit/core или @octokit/request. На данный момент осталось несколько smoke-тестов и сценариев с использованием @octokit/fixtures.


Будущее


@octokit/rest начинался как пакет для REST API GitHub, но с 17 версии, это будет полноценная библиотека JavaScript, включающая вспомогательные функции для API @octokit/rest, с пагинацией, троттлингом и повторными запросами. Также будут поддерживаться все существующие и будущие стратегии аутентификации и GraphQL, так как он является частью @octokit/core.


В конце, я хотел бы сказать спасибо Фабиану Джейкобсу, Майку де Буру и Джо Галло, которые создали и поддерживали модуль github до того, как он превратился в @octokit/rest.