Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Привет, Хабр, меня зовут Артем Кудряшов, некоторую кучу лет я работаю в 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 достаточно лаконичен, демонстрационные исходники можно посмотреть по ссылке.
Ну а Леонид Иович выглядел вот так: