Привет, Хабр, меня зовут Артем Кудряшов, некоторую кучу лет я работаю в ATI.SU — крупнейшей в России бирже грузоперевозок. В разное время я писал код, управлял командами и занимался другими весёлыми вещами. В статье, что вы видите, хочу рассказать об одном из наших сервисов с интересной функциональностью и крутым, по моему личному мнению, названием — Gaidai. Поехали.

У нас в системе есть уведомления для пользователей: мы сообщаем о новых грузах, новых сделках и разных других активностях. Получать уведомления можно на нашем сайте, в мобильных приложениях, пуш-уведомлениями на смартфоны и в браузеры, SMS сообщениями, письмами на почту. В общем и целом, если вы наш пользователь, и вам хочется что-то узнать — мы вас найдём.

Разных сообщений много: на момент написания статьи мы отправляем почти 350 типов сообщений. Вот так выглядит один из вариантов, который пользователь видит на сайте:

Сообщение на сайте
Сообщение на сайте

Внизу сообщения есть кнопка «Взять груз» — это достаточно простой контрол с редиректом по ссылке, ссылка формируется в момент создания и отправляется на фронт.

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

Поумневшее сообщение в приложении
Поумневшее сообщение в приложении

Здесь кнопки уже зелёные (в контексте статьи это неважно) — важно, что их стало две, и они уже не просто «хранители редиректов», а стали немного умней и при нажатии выписывают кульбиты:

Кнопка что-то сделала
Кнопка что-то сделала

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

Мы понимали, что решить задачу можно несколькими вариантами: самым популярным в обсуждении был какой-нибудь Backend for Frontend сервис, но появилась идея и захотелось более универсального решения. Так и появился Gaidai.

Gaidai cервис HTTP сценариев. Сценариями мы назвали последовательность вызовов методов, смысл слов «сервис» и «HTTP» мы заново не придумывали.

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

Основные понятия:

  • сервис — просто сервис, умеет работать по HTTP, для полной функциональности лучше бы общался JSON'ом;

  • метод — метод сервиса (naming — моё всё);

  • сценарий — именованная настраиваемая последовательность методов.

Сервис

Для описания сервисов в Gaidai используется отдельный JSON файл servicessetting.json.

Пример для статьи придуман по ходу написания — чтобы понимать весёлые аллегории, стоит пояснить название сервиса.

У нас в команде сложилась традиция называть сервисы именами исторических и не только персонажей. Так как сервис описывает сценарии, нам хотелось найти сценариста, желательно соотечественника. Леонид Иович Гайдай — знаменитый российский режиссёр, который помимо режиссёрского дела занимался и написанием сценариев к своим фильмам, поэтому пример построен частично на его фильмографии.

{
  "Services": [
    {
      "Id": "DiamondArm",
      "Description": "slipped, fell, woke up - plaster",
      "Path": "http://diamond-arm:1968/",
      "AdditionalHeaders": {
        "Main-Male-Role": "Nikulin",
        "Supporting-Male-Role": "Mironov"
      },
      "TransitHeaders": [
        {
          "Name": "Origin-country",
          "Required": true
        },
        {
          "Name": "Operator-name",
          "Required": false
        }
      ],
      "AdditionalCookies": {
        "Main-Female-Role": "Grebeshkova",
        "Supporting-Female-Role": "Mordyukova"
      },
      "TransitCookies": [
        {
          "Name": "Сomposer-name",
          "Required": false
        }
      ]
    }
  ]
}
  • Id — поле, идентифицирующее сервис. В моём примере сервис будет называться «БриллиантоваяРука», ну или как его окрестил Google Translate — DiamondArm. Именно под таким именем далее по тексту методы будут связываться с их сервисом;

  • Description — поле на самом деле бесполезное, в сервисе фактически не используется, но для описания и понимания полезно;

  • Path — путь к вашему сервису: внутри вашей сети, через nginx, Consul, IP адрес — как вам удобно, лишь бы сервис был доступен;

  • AdditionalHeaders — коллекция ключ-значение, каждая из описанных пар будет добавлена к каждому запросу в этот сервис в виде хэдера. Неплохо подходит для передачи принятых у вас в компании стандартов;

  • TransitHeaders — коллекция ключ-значение, хэдэры здесь транзитные, так как по именам будут браться из исходного запроса клиента и передаваться дальше по цепочке вызовов. Если установлен Required:true, то его отсутствие в исходном запросе приведёт к пользовательской ошибке;

  • AdditionalCookies — логика очень похожа на AdditionalHeaders: всё указанное здесь будет в качестве cookie добавлено ко всем запросам (да, можно заменить хэдером Cookie, но так, кажется, нагляднее);

  • TransitCookies — смотри выше и фантазируй.

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

Метод

Для описания методов тоже используется отдельный файлик methodssettings.json.

{
  "Methods": [
    {
      "Id": "WatchMovie",
      "Description": "Bought popcorn and sat down to watch a movie",
      "ServiceId": "DiamondArm",
      "Url": "v1/watch?watcherId={0}",
      "HttpMethod": "Post",
      "Parameters": [
        {
          "Name": "WatcherId",
          "Required": true,
          "Destination": "Url",
          "UrlPosition": 0
        },
        {
          "Name": "StreamingService",
          "Required": false,
          "Destination": "Body",
          "UnnamedBody": false
        }
      ],
      "AdditionalHeaders": {},
      "TransitHeaders": [],
      "AdditionalCookies": {},
      "TransitCookies": []
    },
    {
      "Id": "GetMovieRecommendation",
      "Description": "Get recommendation",
      "ServiceId": "OperationЫ",
      "Url": "v1/recommendation/{0}",
      "HttpMethod": "Get",
      "Parameters": [
        {
          "Name": "movie_id",
          "Required": true,
          "Destination": "Url",
          "UrlPosition": 0,
          "SourceMethod": "WatchMovie"
        }
      ]
    }
  ]
}

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

  • ServiceId — связь с сервисом, следует указывать один из описанных в servicessettings. Если ошибётесь, валидатор на старте вам сообщит.

  • HttpMethod — тут всё просто, все основные HTTP методы поддерживаются.

  • Url — путь к самому методу, поддерживает в том числе заполнители, например, в виде watcherId={0}, ниже будет описание как с ними работать.

  • Parameters — коллекция, которая, пожалуй, наиболее ярко иллюстрирует концепцию сервиса Gaidai. На полях в этом блоке остановимся поподробнее.

    • Name — уникальное имя параметра;

    • Required — работает аналогично одноименному параметру, описанному выше: если true — параметр обязательный и где-то его нужно взять;

    • Destination — место, куда параметр со значением нужно вставить, поддерживаются значения Url, Body, Header. В соответствующее место значение вставится с именем, указанным в Name;

    • UrlPosition — используется, если Destination = Url, вставится по порядку заполнителей, например, для watcherId={0}, правильный UrlPosition = 0;

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

Параметры методов и источники данных — самый мощный инструмент этого подхода, в процессе развития он становится всё более гибким и позволяет создавать комбинации, которые требовали бы достаточно большого количества кода в Backend for Frontend паттерне.

Внимательный чтец поймёт, что второй метод в примере описан неправильно — нет соответствующего сервиса «OperationЫ», которому он принадлежит: всё верно, работать не будет. Но это все таки искусственный пример, да и сервисы разбиты странно, зато теперь мы знаем, что вы очень внимательны.

Сценарий

Мы подобрались к финальной части декларативного подхода, сервисы описаны, методы указаны, осталось всё это сгруппировать в сценарий и уволить программистов — мы почти в мире low-code.

Место хранения сценариев в сервисе нехитрое — scenariossettings.json.

{
  "Scenarios": [
    {
      "Id": "WatchMovieAndGetRecommendation",
      "Description": "Watch the movie and get recommendation",
      "Methods": [
        "WatchMovie",
        "GetMovieRecommendation"
      ],
      "IsDependent": true,
      "IsForgettable": false,
      "IsCommonSuccessful": true,
      "IsRewritableResponse": false,
      "ResponseMethods": [
        "WatchMovie",
        "GetMovieRecommendation"
      ],
      "ResponseFieldTransformation": {
        "Result": "transformed_result",
        "watch_result.field_1": "allResults.transformed_field_1",
        "recommendationResult.field_2": "results.fields.one_more_level.field_666"
      }
    }
  ]
}
  • Id — уникальное имя сценария, именно его будет использовать вызывающая сторона;

  • Methods — список методов, которые будут вызваны при выполнении сценария;

  • IsDependent — при указании этого флага методы будут выполняться в строгой последовательности, указанной в Methods;

  • IsForgettable — если флаг true, то сервис не будет ждать выполнения методов в сценарии, а просто запустит его и вернёт вызывающей стороне 200-ый статус;

  • IsCommonSuccessful — флаг отвечает за то, что будет считаться успешным выполнением сценария. Если true, то 200 в ответе будет только если все методы завершились успешно. В противном случае статусом ответа будет результат последнего метода;

  • ResponseMethods список методов сценария, тела ответов которых будут включены в ответ всего сценария: в примере сверху клиент получит ответы обоих методов;

  • IsRewritableResponse — если false, то ответы методов сценария будут включаться в результирующий ответ под именем метода, не пересекаясь друг с другом. Если true, то данные попадут в корень JSON и будут перезаписаны в последовательности из Method;

  • ResponseFieldTransformation бывает, что у сервисов разные контракты, это поле позволяет трансформировать имена полей ответов, задавая им произвольный нэйминг и произвольную вложенность. Ключ — имя в ответе метода, значение — новая вложенность и новое имя этого поля, можно менять регистр, структуру и вообще веселиться как угодно.

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

Всё описано — у нас есть сервис, методы и сценарий, представим, что недостающий сервис «OperationЫ» у нас тоже есть.

По умолчанию Gaidai стартует на порту 1923 и у него один функциональный метод:

POST http://localhost:1923/v1/scenarios/{{scenarioId}}

ScenarioId в нашем примере следует заменить на «WatchMovieAndGetRecommendation», в тело запроса передать параметры, необходимые для всех ваших методов сценария, а в заголовках то, что вы хотите прокинуть дальше.

Заключение

Сервис Gaidai достаточно лаконичен, демонстрационные исходники можно посмотреть по ссылке.

Ну а Леонид Иович выглядел вот так:

Леонид Гайдай
Леонид Гайдай

Комментарии (1)


  1. DabjeilQutwyngo
    23.02.2022 03:15

    Чем решённая вами задача отличается от спектра задач, решаемых WSDL и SOAP?