Вы когда-нибудь задумывались, почему мы создаем и поддерживаем богатую информацию о типах и моделях на уровнях API и базы данных только для того, чтобы избавиться от всего этого при переходе через HTTP-разделение на веб-клиенты и собственные клиенты? Мы в продуктовой команде Walmart Labs заинтересованы во многих идеях, которые воплощают в жизнь GraphQL, Falcor и подобные инструменты. Система типов GraphQL, существующая как на клиенте, так и на сервере, и прозрачная модель доступа Falcor по сети обеспечивают очень мощные и доступные способы создания и обслуживания продуктов.

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

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

В частности, у моей команды есть документация Swagger, созданная на основе самой реализации API. Мы можем использовать эту информацию и передать ее в инструменты генерации кода JavaScript, такие как Babel и jscodeshift, для генерации нашего клиентского кода. С помощью этого сгенерированного кода, аннотированного типами Flow, мы быстро построили прочный мост между пользовательским интерфейсом и моделями сервера.

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

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

В Rails есть ActiveRecord.
У Hapi есть проверки Joi.
У GraphQL есть своя система типов.

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

Но зачем на этом останавливаться? Зачем отбрасывать интеграцию инструментов и безопасность контракта API в пользу какой-то скудной документации при разделении сети?

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

  • Как сделать взаимодействие API предсказуемым и автоматическим?
  • Как мы можем включить статический анализ кода пользовательского интерфейса в контракты запросов и ответов API?

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

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

Инструменты

  • Swagger генерирует обширную документацию по реализации.
  • jscodeshift для преобразования представления Swagger в абстрактное синтаксическое дерево JavaScript (AST) и исходный файл.

Мы будем использовать публичное определение Swagger компании IBM Watson. Вы можете увидеть это в HTML-формате по адресу https://watson-api-explorer.mybluemix.net/apis/text-to-speech-v1

Секрет в том, что Swagger также генерирует представление JSON для этого. Https://watson-api-explorer.mybluemix.net/listings/text-to-speech-v1.json

Как программисты, мы привыкли работать со структурами данных, такими как объекты и массивы, и преобразовывать их.

… если бы только мы могли преобразовать формат JSON Swagger в файл JavaScript…

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

Чрезмерно краткое введение в AST

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

Древовидная структура - это просто набор узлов. В JavaScript есть узлы для таких вещей, как идентификаторы, декларации переменных, выражения функций, ReturnStatements и все остальные конструкции языка. В качестве краткого примера вы можете посмотреть следующую функцию hello world и ее представление AST.



function hello () {
 return “world!”;
}

Вышеупомянутая функция может быть представлена ​​в виде следующей структуры AST:

{
  "type": "FunctionDeclaration",
  "id": {
    "type": "Identifier",
    "name": "hello"
  },
  "params”: [],
  "body": {
    "type": "BlockStatement",
    "body": [
      {
        "type": "ReturnStatement",
        "argument": {
          "type": "Literal",
          "value": "world!",
          "raw": "\"world!\""
        }
      }
    ]
  }
}

Собираем кусочки вместе

Мы взглянули на модель Swagger JSON, а также на то, как выглядит исходный код программы JS. Теперь напишем программу, которая преобразует модель Swagger в наш JavaScript AST и распечатает ее в файл.

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

type CustomType = any;
export function getV1Resource(
  arg1 : string,
  arg2 : string,
  arg3 : CustomType
) {}

Для генерации всего определения Swagger требуется всего около 130 строк кода. Это 130 строк закомментированного кода, включая запрос и анализ самого JSON.

Https://gist.github.com/iamdustan/2e36a440b5702e14376bfba56d303427

Прочтите это. Поначалу это может показаться немного чуждым, но это легко. Обещать!

Его суть действительно такая маленькая:

/**
 * Generate a JS AST from a Swagger entry.
 */
const toFn = (path, method, obj) => Object.assign(
  j.exportDeclaration(false, j.functionDeclaration(
    j.identifier(genFnName(method, path)),
    genArgs(obj.parameters),
    j.blockStatement([])
 )),
 {comments: [j.commentBlock(formatDescription(obj.description))]}
);

Мы в Walmart Labs заядлые пользователи Redux, поэтому теперь мы сделаем возвращаемое значение экспортированного объявления функции простым определением объекта.

Давайте закончим этот тур написанием кода для создания следующего тела функции:

return {
  type: 'FETCH',
  payload: {}
};

Это ReturnStatement, значение которого - ObjectExpression с двумя ключами.

const genBody = (path, method, obj) =>
  j.blockStatement([j.returnStatement(
    j.objectExpression([
      j.property(‘init’, j.identifier(‘type’), j.literal(‘FETCH’)),
      j.property(‘init’, j.identifier(‘payload’), j.objectExpression([]))
   ])
 )]);

Будущие задачи

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

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

Увы, наши определения типов для возвращаемых значений содержат довольно много потерь. Например, если допустимый ввод {a: string} | {b: SomeEnum}, то в настоящее время создается {a: string, b: SomeEnum}, что совершенно другое. В настоящее время мы вручную исправляем эти случаи. Теперь, когда наш API стабилизировался, мы решили отказаться от автоматически сгенерированных типов и перейти к определениям, поддерживаемым вручную.

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

Теперь, если я смогу найти способ получить в JavaScript исчерпывающее
сопоставление с образцом в Rust…

Спасибо Феликсу Клингу и Джо Хадсону за обзор и отзывы на эту статью.