You've already forked The-API-Book
mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-07-12 22:50:21 +02:00
style fix
This commit is contained in:
468
docs/API.ru.html
468
docs/API.ru.html
@ -94,6 +94,7 @@ body {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.display-none {
|
||||
@ -186,7 +187,6 @@ h5,
|
||||
h6 {
|
||||
font-family: local-serif, serif;
|
||||
font-size: 14pt;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
h6 {
|
||||
@ -855,7 +855,8 @@ GET /v1/orders/{id}
|
||||
<ul>
|
||||
<li>
|
||||
<p>Машины с предустановленными программами:</p>
|
||||
<pre><code>// Возвращает список предустановленных программ
|
||||
<pre><code>// Возвращает список
|
||||
// предустановленных программ
|
||||
GET /programs
|
||||
→
|
||||
{
|
||||
@ -865,7 +866,8 @@ GET /programs
|
||||
"type": "lungo"
|
||||
}
|
||||
</code></pre>
|
||||
<pre><code>// Запускает указанную программу на исполнение
|
||||
<pre><code>// Запускает указанную
|
||||
// программу на исполнение
|
||||
// и возвращает статус исполнения
|
||||
POST /execute
|
||||
{
|
||||
@ -876,7 +878,8 @@ POST /execute
|
||||
{
|
||||
// Уникальный идентификатор задания
|
||||
"execution_id": "01-01",
|
||||
// Идентификатор исполняемой программы
|
||||
// Идентификатор
|
||||
// исполняемой программы
|
||||
"program": 1,
|
||||
// Запрошенный объём напитка
|
||||
"volume": "200ml"
|
||||
@ -886,14 +889,16 @@ POST /execute
|
||||
POST /cancel
|
||||
</code></pre>
|
||||
<pre><code>// Возвращает статус исполнения
|
||||
// Формат аналогичен формату ответа `POST /execute`
|
||||
// Формат аналогичен
|
||||
// формату ответа `POST /execute`
|
||||
GET /execution/status
|
||||
</code></pre>
|
||||
<p><strong>NB</strong>. На всякий случай отметим, что данный API нарушает множество описанных нами принципов проектирования, начиная с отсутствия версионирования; он приведен в таком виде по двум причинам: (1) чтобы мы могли показать, как спроектировать API более удачно; (2) скорее всего, в реальной жизни вы получите именно такой API от производителей кофемашин, и это ещё довольно вменяемый вариант.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Машины с предустановленными функциями:</p>
|
||||
<pre><code>// Возвращает список доступных функций
|
||||
<pre><code>// Возвращает список
|
||||
// доступных функций
|
||||
GET /functions
|
||||
→
|
||||
{
|
||||
@ -905,9 +910,13 @@ GET /functions
|
||||
// * pour_water — пролить воду
|
||||
// * discard_cup — утилизировать стакан
|
||||
"type": "set_cup",
|
||||
// Допустимые аргументы для каждой операции
|
||||
// Для простоты ограничимся одним аргументом:
|
||||
// * volume — объём стакана, кофе или воды
|
||||
// Допустимые аргументы
|
||||
// для каждой операции
|
||||
// Для простоты ограничимся
|
||||
// одним аргументом:
|
||||
// * volume
|
||||
// — объём стакана,
|
||||
// кофе или воды
|
||||
"arguments": ["volume"]
|
||||
},
|
||||
…
|
||||
@ -915,11 +924,15 @@ GET /functions
|
||||
}
|
||||
</code></pre>
|
||||
<pre><code>// Запускает на исполнение функцию
|
||||
// с передачей указанных значений аргументов
|
||||
// с передачей указанных
|
||||
// значений аргументов
|
||||
POST /functions
|
||||
{
|
||||
"type": "set_cup",
|
||||
"arguments": [{ "name": "volume", "value": "300ml" }]
|
||||
"arguments": [{
|
||||
"name": "volume",
|
||||
"value": "300ml"
|
||||
}]
|
||||
}
|
||||
</code></pre>
|
||||
<pre><code>// Возвращает статусы датчиков
|
||||
@ -929,9 +942,12 @@ GET /sensors
|
||||
"sensors": [
|
||||
{
|
||||
// Допустимые значения
|
||||
// * cup_volume — объём установленного стакана
|
||||
// * ground_coffee_volume — объём смолотого кофе
|
||||
// * cup_filled_volume — объём напитка в стакане
|
||||
// * cup_volume
|
||||
// — объём установленного стакана
|
||||
// * ground_coffee_volume
|
||||
// — объём смолотого кофе
|
||||
// * cup_filled_volume
|
||||
// — объём напитка в стакане
|
||||
"type": "cup_volume",
|
||||
"value": "200ml"
|
||||
},
|
||||
@ -1003,7 +1019,7 @@ GET /sensors
|
||||
<p>Обратите внимание, что во всей этой цепочке вообще никак не участвует тип API кофемашины — собственно, ровно для этого мы и абстрагировали. Мы могли бы сделать интерфейсы более конкретными, разделив функциональность <code>run</code> и <code>match</code> для разных API, т.е. ввести раздельные endpoint-ы:</p>
|
||||
<ul>
|
||||
<li><code>POST /v1/program-matcher/{api_type}</code></li>
|
||||
<li><code>POST /v1/programs/{api_type}/{program_id}/run</code></li>
|
||||
<li><code>POST /v1/{api_type}/programs/{id}/run</code></li>
|
||||
</ul>
|
||||
<p>Достоинством такого подхода была бы возможность передавать в match и run не унифицированные наборы параметров, а только те, которые имеют значение в контексте указанного типа API. Однако в нашем дизайне API такой необходимости не прослеживается. Обработчик <code>run</code> сам может извлечь нужные параметры из мета-информации о программе и выполнить одно из двух действий:</p>
|
||||
<ul>
|
||||
@ -1012,7 +1028,11 @@ GET /sensors
|
||||
</ul>
|
||||
<p>Уровень рантаймов API второго типа, исходя из общих соображений, будет скорее всего непубличным, и мы плюс-минус свободны в его имплементации. Самым простым решением будет реализовать виртуальную state-машину, которая создаёт «рантайм» (т.е. stateful контекст исполнения) для выполнения программы и следит за его состоянием.</p>
|
||||
<pre><code>POST /v1/runtimes
|
||||
{ "coffee_machine", "program", "parameters" }
|
||||
{
|
||||
"coffee_machine",
|
||||
"program",
|
||||
"parameters"
|
||||
}
|
||||
→
|
||||
{ "runtime_id", "state" }
|
||||
</code></pre>
|
||||
@ -1038,15 +1058,20 @@ GET /sensors
|
||||
// * "ready_waiting" — напиток готов
|
||||
// * "finished" — все операции завершены
|
||||
"status": "ready_waiting",
|
||||
// Текущая исполняемая команда (необязательное)
|
||||
// Текущая исполняемая команда
|
||||
// (необязательное)
|
||||
"command_sequence_id",
|
||||
// Чем закончилось исполнение программы
|
||||
// (необязательное)
|
||||
// * "success" — напиток приготовлен и выдан
|
||||
// * "terminated" — исполнение остановлено
|
||||
// * "technical_error" — ошибка при приготовлении
|
||||
// * "waiting_time_exceeded" — готовый заказ был
|
||||
// утилизирован, т.к. его не забрали
|
||||
// * "success"
|
||||
// — напиток приготовлен и выдан
|
||||
// * "terminated"
|
||||
// — исполнение остановлено
|
||||
// * "technical_error"
|
||||
// — ошибка при приготовлении
|
||||
// * "waiting_time_exceeded"
|
||||
// — готовый заказ был
|
||||
// утилизирован, его не забрали
|
||||
"resolution": "success",
|
||||
// Значения всех переменных,
|
||||
// включая состояние сенсоров
|
||||
@ -1215,17 +1240,23 @@ GET /sensors
|
||||
</ul>
|
||||
<p>В псевдокоде это будет выглядеть примерно вот так:</p>
|
||||
<pre><code>// Получить все доступные рецепты
|
||||
let recipes = api.getRecipes();
|
||||
let recipes =
|
||||
api.getRecipes();
|
||||
// Получить все доступные кофемашины
|
||||
let coffeeMachines = api.getCoffeeMachines();
|
||||
let coffeeMachines =
|
||||
api.getCoffeeMachines();
|
||||
// Построить пространственный индекс
|
||||
let coffeeMachineRecipesIndex = buildGeoIndex(recipes, coffeeMachines);
|
||||
// Выбрать кофемашины, соответствующие запросу пользователя
|
||||
let matchingCoffeeMachines = coffeeMachineRecipesIndex.query(
|
||||
parameters,
|
||||
{ "sort_by": "distance" }
|
||||
);
|
||||
// Наконец, показать предложения пользователю
|
||||
let coffeeMachineRecipesIndex =
|
||||
buildGeoIndex(recipes, coffeeMachines);
|
||||
// Выбрать кофемашины,
|
||||
// соответствующие запросу пользователя
|
||||
let matchingCoffeeMachines =
|
||||
coffeeMachineRecipesIndex.query(
|
||||
parameters,
|
||||
{ "sort_by": "distance" }
|
||||
);
|
||||
// Наконец, показать
|
||||
// предложения пользователю
|
||||
app.display(coffeeMachines);
|
||||
</code></pre>
|
||||
<p>Как видите, разработчику придётся написать немало лишнего кода (это не упоминая о сложности имплементации геопространственных индексов!). Притом, учитывая наши наполеоновские планы по покрытию нашим API всех кофемашин мира, такой алгоритм выглядит заведомо бессмысленной тратой ресурсов на получение списков и поиск по ним.</p>
|
||||
@ -1247,9 +1278,12 @@ app.display(coffeeMachines);
|
||||
}
|
||||
→
|
||||
{
|
||||
"results": [
|
||||
{ "coffee_machine", "place", "distance", "offer" }
|
||||
],
|
||||
"results": [{
|
||||
"coffee_machine",
|
||||
"place",
|
||||
"distance",
|
||||
"offer"
|
||||
}],
|
||||
"cursor"
|
||||
}
|
||||
</code></pre>
|
||||
@ -1279,12 +1313,15 @@ app.display(offers);
|
||||
<pre><code>{
|
||||
"results": [
|
||||
{
|
||||
"coffee_machine", "place", "distance",
|
||||
"coffee_machine",
|
||||
"place",
|
||||
"distance",
|
||||
"offer": {
|
||||
"id",
|
||||
"price",
|
||||
"currency_code",
|
||||
// Указываем дату и время, до наступления которых
|
||||
// Указываем дату и время,
|
||||
// до наступления которых
|
||||
// предложение является актуальным
|
||||
"valid_until"
|
||||
}
|
||||
@ -1324,10 +1361,12 @@ app.display(offers);
|
||||
// Род ошибки
|
||||
"reason": "offer_invalid",
|
||||
"localized_message":
|
||||
"Что-то пошло не так. Попробуйте перезагрузить приложение."
|
||||
"Что-то пошло не так.⮠
|
||||
Попробуйте перезагрузить приложение."
|
||||
"details": {
|
||||
// Что конкретно неправильно?
|
||||
// Какие из проверок валидности предложения
|
||||
// Какие из проверок
|
||||
// валидности предложения
|
||||
// отработали с ошибкой?
|
||||
"checks_failed": [
|
||||
"offer_lifetime"
|
||||
@ -1343,45 +1382,43 @@ app.display(offers);
|
||||
<p>Бороться с этим законом можно только одним способом: декомпозицией. На каждом уровне работы с вашим API нужно стремиться логически группировать сущности под одним именем там, где это возможно и таким образом, чтобы разработчику никогда не приходилось оперировать более чем 10 сущностями одновременно.</p>
|
||||
<p>Рассмотрим простой пример: что должна возвращать функция поиска подходящей кофемашины. Для обеспечения хорошего UX приложения необходимо передать довольно значительные объёмы информации.</p>
|
||||
<pre><code>{
|
||||
"results": [
|
||||
{
|
||||
"coffee_machine_id",
|
||||
// Тип кофемашины
|
||||
"coffee_machine_type": "drip_coffee_maker",
|
||||
// Марка кофемашины
|
||||
"coffee_machine_brand",
|
||||
// Название заведения
|
||||
"place_name": "Кафе «Ромашка»",
|
||||
// Координаты
|
||||
"place_location_latitude",
|
||||
"place_location_longitude",
|
||||
// Флаг «открыто сейчас»
|
||||
"place_open_now",
|
||||
// Часы работы
|
||||
"working_hours",
|
||||
// Сколько идти: время и расстояние
|
||||
"walking_distance",
|
||||
"walking_time",
|
||||
// Как найти заведение и кофемашину
|
||||
"place_location_tip",
|
||||
"offers": [
|
||||
{
|
||||
"recipe": "lungo",
|
||||
"recipe_name": "Наш фирменный лунго®™",
|
||||
"recipe_description",
|
||||
"volume": "800ml",
|
||||
"offer_id",
|
||||
"offer_valid_until",
|
||||
"localized_price": "Большая чашка всего за 19 баксов",
|
||||
"price": "19.00",
|
||||
"currency_code": "USD",
|
||||
"estimated_waiting_time": "20s"
|
||||
},
|
||||
…
|
||||
]
|
||||
},
|
||||
…
|
||||
]
|
||||
"results": [{
|
||||
"coffee_machine_id",
|
||||
// Тип кофемашины
|
||||
"coffee_machine_type":
|
||||
"drip_coffee_maker",
|
||||
// Марка кофемашины
|
||||
"coffee_machine_brand",
|
||||
// Название заведения
|
||||
"place_name": "Кафе «Ромашка»",
|
||||
// Координаты
|
||||
"place_location_latitude",
|
||||
"place_location_longitude",
|
||||
// Флаг «открыто сейчас»
|
||||
"place_open_now",
|
||||
// Часы работы
|
||||
"working_hours",
|
||||
// Сколько идти: время и расстояние
|
||||
"walking_distance",
|
||||
"walking_time",
|
||||
// Как найти заведение и кофемашину
|
||||
"place_location_tip",
|
||||
"offers": [{
|
||||
"recipe": "lungo",
|
||||
"recipe_name":
|
||||
"Наш фирменный лунго®™",
|
||||
"recipe_description",
|
||||
"volume": "800ml",
|
||||
"offer_id",
|
||||
"offer_valid_until",
|
||||
"localized_price":
|
||||
"Большая чашка⮠
|
||||
всего за 19 баксов",
|
||||
"price": "19.00",
|
||||
"currency_code": "USD",
|
||||
"estimated_waiting_time": "20s"
|
||||
}, …]
|
||||
}, …]
|
||||
}
|
||||
</code></pre>
|
||||
<p>Подход, увы, совершенно стандартный, его можно встретить практически в любом API. Как мы видим, количество полей сущностей вышло далеко за рекомендованные 7, и даже 9. При этом набор полей идёт плоским списком вперемешку, часто с одинаковыми префиксами.</p>
|
||||
@ -1399,22 +1436,36 @@ app.display(offers);
|
||||
<pre><code>{
|
||||
"results": [{
|
||||
// Данные о заведении
|
||||
"place": { "name", "location" },
|
||||
"place":
|
||||
{ "name", "location" },
|
||||
// Данные о кофемашине
|
||||
"coffee-machine": { "id", "brand", "type" },
|
||||
"coffee-machine":
|
||||
{ "id", "brand", "type" },
|
||||
// Как добраться
|
||||
"route": { "distance", "duration", "location_tip" },
|
||||
"route": {
|
||||
"distance",
|
||||
"duration",
|
||||
"location_tip"
|
||||
},
|
||||
// Предложения напитков
|
||||
"offers": [{
|
||||
// Рецепт
|
||||
"recipe": { "id", "name", "description" },
|
||||
"recipe":
|
||||
{ "id", "name", "description" },
|
||||
// Данные относительно того,
|
||||
// как рецепт готовят на конкретной кофемашине
|
||||
"options": { "volume" },
|
||||
// как рецепт готовят
|
||||
// на конкретной кофемашине
|
||||
"options":
|
||||
{ "volume" },
|
||||
// Метаданные предложения
|
||||
"offer": { "id", "valid_until" },
|
||||
"offer":
|
||||
{ "id", "valid_until" },
|
||||
// Цена
|
||||
"pricing": { "currency_code", "price", "localized_price" },
|
||||
"pricing": {
|
||||
"currency_code",
|
||||
"price",
|
||||
"localized_price"
|
||||
},
|
||||
"estimated_waiting_time"
|
||||
}, …]
|
||||
}, …]
|
||||
@ -1496,8 +1547,8 @@ strpbrk (str1, str2)
|
||||
<p>Возможно, автору этого API казалось, что аббревиатура <code>pbrk</code> что-то значит для читателя, но он явно ошибся. К тому же, невозможно сходу понять, какая из строк <code>str1</code>, <code>str2</code> является набором символов для поиска.</p>
|
||||
<p><strong>Хорошо</strong>:</p>
|
||||
<pre><code>str_search_for_characters(
|
||||
lookup_character_set,
|
||||
str
|
||||
str,
|
||||
lookup_character_set
|
||||
)
|
||||
</code></pre>
|
||||
<p>— однако необходимость существования такого метода вообще вызывает сомнения, достаточно было бы иметь удобную функцию поиска подстроки с нужными параметрами. Аналогично сокращение <code>string</code> до <code>str</code> выглядит совершенно бессмысленным, но, увы, является устоявшимся для большого количества предметных областей.</p>
|
||||
@ -1591,25 +1642,25 @@ str_replace(needle, replace, haystack)
|
||||
<p>Если же требуется ввести небулево поле, отсутствие которого трактуется специальным образом, то следует ввести пару полей.</p>
|
||||
<p><strong>Плохо</strong>:</p>
|
||||
<pre><code>// Создаёт пользователя
|
||||
POST /users
|
||||
POST /v1/users
|
||||
{ … }
|
||||
→
|
||||
// Пользователи создаются по умолчанию
|
||||
// с указанием лимита трат в месяц
|
||||
{
|
||||
"spending_monthly_limit_usd": "100",
|
||||
…
|
||||
"spending_monthly_limit_usd": "100"
|
||||
}
|
||||
// Для отмены лимита требуется
|
||||
// указать значение null
|
||||
POST /users
|
||||
PUT /v1/users/{id}
|
||||
{
|
||||
"spending_monthly_limit_usd": null,
|
||||
…
|
||||
"spending_monthly_limit_usd": null
|
||||
}
|
||||
</code></pre>
|
||||
<p><strong>Хорошо</strong></p>
|
||||
<pre><code>POST /users
|
||||
<pre><code>POST /v1/users
|
||||
{
|
||||
// true — у пользователя снят
|
||||
// лимит трат в месяц
|
||||
@ -1627,7 +1678,7 @@ POST /users
|
||||
<h5><a href="#chapter-11-paragraph-9" id="chapter-11-paragraph-9" class="anchor">9. Отсутствие результата — тоже результат</a></h5>
|
||||
<p>Если сервер корректно обработал вопрос и никакой внештатной ситуации не возникло — следовательно, это не ошибка. К сожалению, весьма распространён антипаттерн, когда отсутствие результата считается ошибкой.</p>
|
||||
<p><strong>Плохо</strong></p>
|
||||
<pre><code>POST /search
|
||||
<pre><code>POST /v1/coffee-machines/search
|
||||
{
|
||||
"query": "lungo",
|
||||
"location": <положение пользователя>
|
||||
@ -1640,7 +1691,7 @@ POST /users
|
||||
</code></pre>
|
||||
<p>Статусы <code>4xx</code> означают, что клиент допустил ошибку; однако в данном случае никакой ошибки сделано не было ни пользователем, ни разработчиком: клиент же не может знать заранее, готовят здесь лунго или нет.</p>
|
||||
<p><strong>Хорошо</strong>:</p>
|
||||
<pre><code>POST /search
|
||||
<pre><code>POST /v1/coffee-machines/search
|
||||
{
|
||||
"query": "lungo",
|
||||
"location": <положение пользователя>
|
||||
@ -2254,7 +2305,7 @@ GET /v1/recipes
|
||||
<li>
|
||||
<p>не злоупотребляйте асинхронными интерфейсами;</p>
|
||||
<ul>
|
||||
<li>с одной стороны, они позволяют нивелировать многие технических проблем с производительностью API, что, в свою очередь, позволяет поддерживать обратную совместимость: если метод изначально асинхронный, то можно без проблем увеличивать время обработки и менять модель консистентности данных;</li>
|
||||
<li>с одной стороны, они позволяют нивелировать многие технические проблемы с производительностью API, что, в свою очередь, позволяет поддерживать обратную совместимость: если метод изначально асинхронный, то можно без проблем увеличивать время обработки и менять модель консистентности данных;</li>
|
||||
<li>с другой стороны, количество генерируемых клиентами запросов становится трудно предсказуемым, поскольку для получения результата клиенту необходимо сделать заранее неизвестное число обращений;</li>
|
||||
</ul>
|
||||
</li>
|
||||
@ -2268,7 +2319,7 @@ GET /v1/recipes
|
||||
<p>если вы ожидаете значительного количества асинхронных операций в API, изначально дайте разработчику выбор между моделями poll (клиент самостоятельно производит новые запросы к API чтобы проверить, не изменился ли статус асинхронной операций) и push (сервер уведомляет клиентов об изменениях статусов посредством отправки специального запроса, например, через webhook-и или server push-механизмы);</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>если в рамках одной сущности необходимо предоставлять как «лёгкие» (скажем, название и описание рецепта), так и «тяжёлые» данные (скажем, промо-фотография напитка, которая легко может по разумеру превышать текстовые поля в сотни раз), лучше разделить эндпойнты и отдавать только ссылку для доступа к «тяжёлым» данным (в нашем случае, ссылку на изображение) — это, как минимум, позволит задавать различные политики кэширования для разных данных.</p>
|
||||
<p>если в рамках одной сущности необходимо предоставлять как «лёгкие» (скажем, название и описание рецепта), так и «тяжёлые» данные (скажем, промо-фотография напитка, которая легко может по размеру превышать текстовые поля в сотни раз), лучше разделить эндпойнты и отдавать только ссылку для доступа к «тяжёлым» данным (в нашем случае, ссылку на изображение) — это, как минимум, позволит задавать различные политики кэширования для разных данных.</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p>Неплохим упражнением здесь будет промоделировать типовой жизненный цикл основной функциональности приложения партнёра (например, выполнение одного заказа) и подсчитать общее количество запросов и объём трафика на один цикл.</p>
|
||||
@ -2301,7 +2352,7 @@ PATCH /v1/orders/{id}
|
||||
{ /* изменения приняты */ }
|
||||
</code></pre>
|
||||
<p>Эта сигнатура плоха сама по себе, поскольку является нечитабельной. Что обозначает пустой первый элемент массива — это удаление элемента или указание на отсутствие изменений? Что произойдёт с полями, которые не указаны в операции обновления (<code>delivery_address</code>, <code>milk_type</code>) — они будут сброшены в значения по умолчанию или останутся неизменными?</p>
|
||||
<p>Самое неприятное здесь — какой бы вариант вы ни выбрали, это только начало проблем. Допустим, мы договорились, что конструкция <code>{ "items":[null, {…}] }</code> означает, что с первым элементом массива ничего не происходит, он не меняется. А как тогда всё-таки его удалить? Придумать ещё одно «зануляемое» значение специально для удаления? Аналогично, если значения неуказанных полей остаются без изменений — как сбросить их в значения по умолчанию?</p>
|
||||
<p>Самое неприятное здесь — какой бы вариант вы ни выбрали, это только начало проблем. Допустим, мы договорились, что конструкция <code>{"items":[null, {…}]}</code> означает, что с первым элементом массива ничего не происходит, он не меняется. А как тогда всё-таки его удалить? Придумать ещё одно «зануляемое» значение специально для удаления? Аналогично, если значения неуказанных полей остаются без изменений — как сбросить их в значения по умолчанию?</p>
|
||||
<p><strong>Простое решение</strong> состоит в том, чтобы всегда перезаписывать объект целиком, т.е. требовать передачи полного объекта, полностью заменять им текущее состояние и возвращать в ответ на операцию новое состояние целиком. Однако это простое решение часто не принимается по нескольким причинам:</p>
|
||||
<ul>
|
||||
<li>повышенные размеры запросов и, как следствие, расход трафика;</li>
|
||||
@ -2430,22 +2481,36 @@ X-Idempotency-Token: <токен идемпотентности>
|
||||
{
|
||||
"results": [{
|
||||
// Данные о заведении
|
||||
"place": { "name", "location" },
|
||||
"place":
|
||||
{ "name", "location" },
|
||||
// Данные о кофемашине
|
||||
"coffee_machine": { "id", "brand", "type" },
|
||||
"coffee_machine":
|
||||
{ "id", "brand", "type" },
|
||||
// Как добраться
|
||||
"route": { "distance", "duration", "location_tip" },
|
||||
"route": {
|
||||
"distance",
|
||||
"duration",
|
||||
"location_tip"
|
||||
},
|
||||
// Предложения напитков
|
||||
"offers": [{
|
||||
// Рецепт
|
||||
"recipe": { "id", "name", "description" },
|
||||
"recipe":
|
||||
{ "id", "name", "description" },
|
||||
// Данные относительно того,
|
||||
// как рецепт готовят на конкретной кофемашине
|
||||
"options": { "volume" },
|
||||
// как рецепт готовят
|
||||
// на конкретной кофемашине
|
||||
"options":
|
||||
{ "volume" },
|
||||
// Метаданные предложения
|
||||
"offer": { "id", "valid_until" },
|
||||
"offer":
|
||||
{ "id", "valid_until" },
|
||||
// Цена
|
||||
"pricing": { "currency_code", "price", "localized_price" },
|
||||
"pricing": {
|
||||
"currency_code",
|
||||
"price",
|
||||
"localized_price"
|
||||
},
|
||||
"estimated_waiting_time"
|
||||
}, …]
|
||||
}, …]
|
||||
@ -2463,7 +2528,11 @@ GET /v1/recipes?cursor=<курсор>
|
||||
// по его идентификатору
|
||||
GET /v1/recipes/{id}
|
||||
→
|
||||
{ "recipe_id", "name", "description" }
|
||||
{
|
||||
"recipe_id",
|
||||
"name",
|
||||
"description"
|
||||
}
|
||||
</code></pre>
|
||||
<h5><a href="#chapter-12-paragraph-3" id="chapter-12-paragraph-3" class="anchor">3. Работа с заказами</a></h5>
|
||||
<pre><code>// Размещает заказ
|
||||
@ -2540,7 +2609,11 @@ POST /v1/runs/{id}/cancel
|
||||
<h5><a href="#chapter-12-paragraph-6" id="chapter-12-paragraph-6" class="anchor">6. Управление рантаймами</a></h5>
|
||||
<pre><code>// Создаёт новый рантайм
|
||||
POST /v1/runtimes
|
||||
{ "coffee_machine_id", "program_id", "parameters" }
|
||||
{
|
||||
"coffee_machine_id",
|
||||
"program_id",
|
||||
"parameters"
|
||||
}
|
||||
→
|
||||
{ "runtime_id", "state" }
|
||||
</code></pre>
|
||||
@ -2549,7 +2622,8 @@ POST /v1/runtimes
|
||||
GET /v1/runtimes/{runtime_id}/state
|
||||
{
|
||||
"status": "ready_waiting",
|
||||
// Текущая исполняемая команда (необязательное)
|
||||
// Текущая исполняемая команда
|
||||
// (необязательное)
|
||||
"command_sequence_id",
|
||||
"resolution": "success",
|
||||
"variables"
|
||||
@ -2696,7 +2770,8 @@ while (true) {
|
||||
try {
|
||||
status = api.getStatus(order.id);
|
||||
} catch (e) {
|
||||
if (e.httpStatusCode != 404 || timeoutExceeded()) {
|
||||
if (e.httpStatusCode != 404 ||
|
||||
timeoutExceeded()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -2732,26 +2807,25 @@ object.observe('widthchange', observerFunction);
|
||||
<p><strong>Пример 4</strong>. Представьте, что потребитель совершает заказ, которые проходит через вполне определённую цепочку преобразований:</p>
|
||||
<pre><code>GET /v1/orders/{id}/events/history
|
||||
→
|
||||
{
|
||||
"event_history": [
|
||||
{
|
||||
"iso_datetime": "2020-12-29T00:35:00+03:00",
|
||||
"new_status": "created"
|
||||
},
|
||||
{
|
||||
"iso_datetime": "2020-12-29T00:35:10+03:00",
|
||||
"new_status": "payment_approved"
|
||||
},
|
||||
{
|
||||
"iso_datetime": "2020-12-29T00:35:20+03:00",
|
||||
"new_status": "preparing_started"
|
||||
},
|
||||
{
|
||||
"iso_datetime": "2020-12-29T00:35:30+03:00",
|
||||
"new_status": "ready"
|
||||
}
|
||||
]
|
||||
}
|
||||
{ "event_history": [
|
||||
{
|
||||
"iso_datetime":
|
||||
"2020-12-29T00:35:00+03:00",
|
||||
"new_status": "created"
|
||||
}, {
|
||||
"iso_datetime":
|
||||
"2020-12-29T00:35:10+03:00",
|
||||
"new_status": "payment_approved"
|
||||
}, {
|
||||
"iso_datetime":
|
||||
"2020-12-29T00:35:20+03:00",
|
||||
"new_status": "preparing_started"
|
||||
}, {
|
||||
"iso_datetime":
|
||||
"2020-12-29T00:35:30+03:00",
|
||||
"new_status": "ready"
|
||||
}
|
||||
]}
|
||||
</code></pre>
|
||||
<p>Допустим, в какой-то момент вы решили надёжным клиентам с хорошей историей заказов предоставлять кофе «в кредит», не дожидаясь подтверждения платежа. Т.е. заказ перейдёт в статус <code>"preparing_started"</code>, а может и <code>"ready"</code>, вообще без события <code>"payment_approved"</code>. Вам может показаться, что это изменение является обратно-совместимым — в самом деле, вы же и не обещали никакого конкретного порядка событий. Но это, конечно, не так.</p>
|
||||
<p>Предположим, что у разработчика (вероятно, бизнес-партнёра вашей компании) написан какой-то код, выполняющий какую-то полезную бизнес функцию поверх этих событий — например, строит аналитику по затратам и доходам. Вполне логично ожидать, что этот код будет оперировать какой-то машиной состояний, которая будет переходить в то или иное состояние в зависимости от получения или неполучения события. Аналитический код наверняка сломается вследствие изменения порядка событий. В лучшем случае разработчик увидит какие-то исключения и будет вынужден разбираться с причиной; в худшем случае партнёр будет оперировать неправильной статистикой неопределённое время, пока не найдёт в ней ошибку.</p>
|
||||
@ -2821,7 +2895,8 @@ PUT /v1/partners/{partnerId}/coffee-machines
|
||||
</li>
|
||||
<li>
|
||||
<p>Добавляем новый метод <code>with-options</code>:</p>
|
||||
<pre><code>PUT /v1/partners/{partner_id}/coffee-machines-with-options
|
||||
<pre><code>PUT /v1/partners/{partner_id}⮠
|
||||
/coffee-machines-with-options
|
||||
{
|
||||
"coffee_machines": [{
|
||||
"id",
|
||||
@ -2888,9 +2963,15 @@ POST /v1/recipes
|
||||
<h4>Правило контекстов</h4>
|
||||
<p>Как бы парадоксально это ни звучало, обратное утверждение тоже верно: высокоуровневые сущности тоже не должны определять низкоуровневые. Это попросту не их ответственность. Выход из этого логического лабиринта таков: высокоуровневые сущности должны <em>определять контекст</em>, который другие объекты будут интерпретировать. Чтобы спроектировать добавление нового рецепта нам нужно не формат данных подобрать — нам нужно понять, какие (возможно, неявные, т.е. не представленные в виде API) контексты существуют в нашей предметной области.</p>
|
||||
<p>Как уже понятно, существует контекст локализации. Есть какой-то набор языков и регионов, которые мы поддерживаем в нашем API, и есть требования — что конкретно необходимо предоставить партнёру, чтобы API заработал на новом языке в новом регионе. Конкретно в случае объёма кофе где-то в недрах нашего API есть функция форматирования строк для отображения объёма напитка:</p>
|
||||
<pre><code>l10n.volume.format(value, language_code, country_code)
|
||||
// l10n.formatVolume('300ml', 'en', 'UK') → '300 ml'
|
||||
// l10n.formatVolume('300ml', 'en', 'US') → '10 fl oz'
|
||||
<pre><code>l10n.volume.format(
|
||||
value, language_code, country_code
|
||||
)
|
||||
// l10n.formatVolume(
|
||||
// '300ml', 'en', 'UK'
|
||||
// ) → '300 ml'
|
||||
// l10n.formatVolume(
|
||||
// '300ml', 'en', 'US'
|
||||
// ) → '10 fl oz'
|
||||
</code></pre>
|
||||
<p>Чтобы наш API корректно заработал с новым языком или регионом, партнёр должен или задать эту функцию, или указать, какую из существующих локализаций необходимо использовать. Для этого мы абстрагируем-и-расширяем API, в соответствии с описанной в предыдущей главе процедурой, и добавляем новый эндпойнт — настройки форматирования:</p>
|
||||
<pre><code>// Добавляем общее правило форматирования
|
||||
@ -2917,7 +2998,8 @@ PUT /formatters/volume/ru/US
|
||||
<pre><code>GET /v1/layouts/{layout_id}
|
||||
{
|
||||
"id",
|
||||
// Макетов вполне возможно будет много разных,
|
||||
// Макетов вполне возможно
|
||||
// будет много разных,
|
||||
// поэтому имеет смысл сразу заложить
|
||||
// расширяемость
|
||||
"kind": "recipe_search",
|
||||
@ -2928,7 +3010,8 @@ PUT /formatters/volume/ru/US
|
||||
// Раз уж мы договорились, что `name`
|
||||
// на самом деле нужен как заголовок
|
||||
// в списке результатов поиска —
|
||||
// разумнее его так и назвать `search_title`
|
||||
// разумнее его так и назвать
|
||||
// `search_title`
|
||||
"field": "search_title",
|
||||
"view": {
|
||||
// Машиночитаемое описание того,
|
||||
@ -2939,11 +3022,15 @@ PUT /formatters/volume/ru/US
|
||||
}
|
||||
}, …],
|
||||
// Какие поля обязательны
|
||||
"required": ["search_title", "search_description"]
|
||||
"required": [
|
||||
"search_title",
|
||||
"search_description"
|
||||
]
|
||||
}
|
||||
</code></pre>
|
||||
<p>Таким образом, партнёр сможет сам решить, какой вариант ему предпочтителен. Можно задать необходимые поля для стандартного макета:</p>
|
||||
<pre><code>PUT /v1/recipes/{id}/properties/l10n/{lang}
|
||||
<pre><code>PUT /v1/recipes/{id}/⮠
|
||||
properties/l10n/{lang}
|
||||
{
|
||||
"search_title", "search_description"
|
||||
}
|
||||
@ -2979,8 +3066,14 @@ PUT /formatters/volume/ru/US
|
||||
// Добавляем нужные форматтеры
|
||||
"formatters": {
|
||||
"volume": [
|
||||
{ "language_code", "template" },
|
||||
{ "language_code", "country_code", "template" }
|
||||
{
|
||||
"language_code",
|
||||
"template"
|
||||
}, {
|
||||
"language_code",
|
||||
"country_code",
|
||||
"template"
|
||||
}
|
||||
]
|
||||
},
|
||||
// Прочие действия, которые необходимо
|
||||
@ -3000,7 +3093,8 @@ PUT /formatters/volume/ru/US
|
||||
}
|
||||
→
|
||||
{
|
||||
"id": "my-coffee-company:lungo-customato"
|
||||
"id":
|
||||
"my-coffee-company:lungo-customato"
|
||||
}
|
||||
</code></pre>
|
||||
<p>Заметим, что в таком формате мы сразу закладываем важное допущение: различные партнёры могут иметь как полностью изолированные неймспейсы, так и разделять их. Более того, мы можем ввести специальные неймспейсы типа "common", которые позволят публиковать новые рецепты для всех. (Это, кстати говоря, хорошо ещё и тем, что такой API мы сможем использовать для организации нашей собственной панели управления контентом.)</p><div class="page-break"></div><h3><a href="#chapter-17" class="anchor" id="chapter-17">Глава 17. Слабая связность</a></h3>
|
||||
@ -3076,24 +3170,33 @@ PUT /v1/api-types/{api_type}
|
||||
<p>Организовать и то, и другое можно разными способами, однако по сути мы имеем два описания состояния (верхне- и низкоуровневое) и поток событий между ними. В случае SDK эту идею можно было бы выразить так:</p>
|
||||
<pre><code>/* Имплементация партнёром интерфейса
|
||||
запуска программы на его кофемашинах */
|
||||
registerProgramRunHandler(apiType, (context) => {
|
||||
// Инициализируем запуск исполнения
|
||||
// программы на стороне партнёра
|
||||
let execution = initExecution(context, …);
|
||||
// Подписываемся на события
|
||||
// изменения контекста
|
||||
context.on('takeout_requested', () => {
|
||||
// Если запрошена выдача напитка,
|
||||
// инициализируем выдачу
|
||||
execution.prepareTakeout(() => {
|
||||
// как только напиток готов к выдаче,
|
||||
// сигнализируем об этом
|
||||
execution.context.emit('takeout_ready');
|
||||
});
|
||||
});
|
||||
registerProgramRunHandler(
|
||||
apiType,
|
||||
(context) => {
|
||||
// Инициализируем запуск исполнения
|
||||
// программы на стороне партнёра
|
||||
let execution =
|
||||
initExecution(context, …);
|
||||
// Подписываемся на события
|
||||
// изменения контекста
|
||||
context.on(
|
||||
'takeout_requested',
|
||||
() => {
|
||||
// Если запрошена выдача напитка,
|
||||
// инициализируем выдачу
|
||||
execution.prepareTakeout(() => {
|
||||
// как только напиток
|
||||
// готов к выдаче,
|
||||
// сигнализируем об этом
|
||||
execution.context
|
||||
.emit('takeout_ready');
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
return execution.context;
|
||||
});
|
||||
return execution.context;
|
||||
}
|
||||
);
|
||||
</code></pre>
|
||||
<p><strong>NB</strong>: в случае HTTP API соответствующий пример будет выглядеть более громоздко, поскольку потребует создания отдельных эндпойнтов чтения очередей событий типа <code>GET /program-run/events</code> и <code>GET /partner/{id}/execution/events</code>, это упражнение мы оставляем читателю. Следует также отметить, что в реальных системах потоки событий часто направляют через внешнюю шину типа Apache Kafka или Amazon SNS/SQS.</p>
|
||||
<p>Внимательный читатель может возразить нам, что фактически, если мы посмотрим на номенклатуру возникающих сущностей, мы ничего не изменили в постановке задачи, и даже усложнили её:</p>
|
||||
@ -3115,26 +3218,37 @@ registerProgramRunHandler(apiType, (context) => {
|
||||
<p>Как несложно понять из вышесказанного, двусторонняя слабая связь означает существенное усложнение имплементации обоих уровней, что во многих ситуациях может оказаться излишним. Часто двустороннюю слабую связь можно без потери качества заменить на одностороннюю, а именно — разрешить нижележащей сущности вместо генерации событий напрямую вызывать методы из интерфейса более высокого уровня. Наш пример изменится примерно вот так:</p>
|
||||
<pre><code>/* Имплементация партнёром интерфейса
|
||||
запуска программы на его кофемашинах */
|
||||
registerProgramRunHandler(apiType, (context) => {
|
||||
// Инициализируем запуск исполнения
|
||||
// программы на стороне партнёра
|
||||
let execution = initExecution(context, …);
|
||||
// Подписываемся на события
|
||||
// изменения контекста
|
||||
context.on('takeout_requested', () => {
|
||||
// Если запрошена выдача напитка,
|
||||
// инициализируем выдачу
|
||||
execution.prepareTakeout(() => {
|
||||
/* как только напиток готов к выдаче,
|
||||
сигнализируем об этом, но не
|
||||
посредством генерации события */
|
||||
// execution.context.emit('takeout_ready')
|
||||
context.set('takeout_ready');
|
||||
// Или ещё более жёстко:
|
||||
// context.setTakeoutReady();
|
||||
});
|
||||
});
|
||||
// Так как мы сами изменяем родительский контекст
|
||||
registerProgramRunHandler(
|
||||
apiType,
|
||||
(context) => {
|
||||
// Инициализируем запуск исполнения
|
||||
// программы на стороне партнёра
|
||||
let execution =
|
||||
initExecution(context, …);
|
||||
// Подписываемся на события
|
||||
// изменения контекста
|
||||
context.on(
|
||||
'takeout_requested',
|
||||
() => {
|
||||
// Если запрошена выдача напитка,
|
||||
// инициализируем выдачу
|
||||
execution.prepareTakeout(() => {
|
||||
/* как только напиток
|
||||
готов к выдаче,
|
||||
сигнализируем об этом,
|
||||
вызовом метода контекста,
|
||||
а не генерацией события */
|
||||
// execution.context
|
||||
// .emit('takeout_ready')
|
||||
context.set('takeout_ready');
|
||||
// Или ещё более жёстко:
|
||||
// context.setTakeoutReady();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
// Так как мы сами
|
||||
// изменяем родительский контекст
|
||||
// нет нужды что-либо возвращать
|
||||
// return execution.context;
|
||||
}
|
||||
|
@ -81,6 +81,7 @@ body {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.display-none {
|
||||
@ -173,7 +174,6 @@ h5,
|
||||
h6 {
|
||||
font-family: local-serif, serif;
|
||||
font-size: 14pt;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
h6 {
|
||||
|
@ -108,7 +108,13 @@ strpbrk (str1, str2)
|
||||
|
||||
Possibly, an author of this API thought that the `pbrk` abbreviature would mean something to readers; clearly mistaken. Also, it's hard to tell from the signature which string (`str1` or `str2`) stands for a character set.
|
||||
|
||||
**Better**: `str_search_for_characters (lookup_character_set, str)`
|
||||
**Better**:
|
||||
```
|
||||
str_search_for_characters(
|
||||
str,
|
||||
lookup_character_set
|
||||
)
|
||||
```
|
||||
— though it's highly disputable whether this function should exist at all; a feature-rich search function would be much more convenient. Also, shortening a `string` to an `str` bears no practical sense, regretfully being a routine in many subject areas.
|
||||
|
||||
**NB**: sometimes field names are shortened or even omitted (e.g., a heterogenous array is passed instead of a set of named fields) to lessen the amount of traffic. In most cases, this is absolutely meaningless as usually the data is compressed at the protocol level.
|
||||
@ -234,26 +240,26 @@ If a non-Boolean field with specially treated value absence is to be introduced,
|
||||
**Bad**:
|
||||
```
|
||||
// Creates a user
|
||||
POST /users
|
||||
POST /v1/users
|
||||
{ … }
|
||||
→
|
||||
// Users are created with a monthly
|
||||
// spending limit set by default
|
||||
{
|
||||
"spending_monthly_limit_usd": "100",
|
||||
…
|
||||
"spending_monthly_limit_usd": "100"
|
||||
}
|
||||
// To cancel the limit null value is used
|
||||
POST /users
|
||||
PUT /v1/users/{id}
|
||||
{
|
||||
"spending_monthly_limit_usd": null,
|
||||
…
|
||||
"spending_monthly_limit_usd": null
|
||||
}
|
||||
```
|
||||
|
||||
**Better**
|
||||
```
|
||||
POST /users
|
||||
POST /v1/users
|
||||
{
|
||||
// true — user explicitly cancels
|
||||
// monthly spending limit
|
||||
@ -276,7 +282,7 @@ If a server processed a request correctly and no exceptional situation occurred
|
||||
|
||||
**Bad**
|
||||
```
|
||||
POST /search
|
||||
POST /v1/coffee-machines/search
|
||||
{
|
||||
"query": "lungo",
|
||||
"location": <customer's location>
|
||||
@ -292,7 +298,7 @@ POST /search
|
||||
|
||||
**Better**:
|
||||
```
|
||||
POST /search
|
||||
POST /v1/coffee-machines/search
|
||||
{
|
||||
"query": "lungo",
|
||||
"location": <customer's location>
|
||||
|
@ -48,7 +48,8 @@ while (true) {
|
||||
try {
|
||||
status = api.getStatus(order.id);
|
||||
} catch (e) {
|
||||
if (e.httpStatusCode != 404 || timeoutExceeded()) {
|
||||
if (e.httpStatusCode != 404 ||
|
||||
timeoutExceeded()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -100,26 +101,25 @@ In this example, you should document the concrete contract (how often the observ
|
||||
```
|
||||
GET /v1/orders/{id}/events/history
|
||||
→
|
||||
{
|
||||
"event_history": [
|
||||
{
|
||||
"iso_datetime": "2020-12-29T00:35:00+03:00",
|
||||
"new_status": "created"
|
||||
},
|
||||
{
|
||||
"iso_datetime": "2020-12-29T00:35:10+03:00",
|
||||
"new_status": "payment_approved"
|
||||
},
|
||||
{
|
||||
"iso_datetime": "2020-12-29T00:35:20+03:00",
|
||||
"new_status": "preparing_started"
|
||||
},
|
||||
{
|
||||
"iso_datetime": "2020-12-29T00:35:30+03:00",
|
||||
"new_status": "ready"
|
||||
}
|
||||
]
|
||||
}
|
||||
{ "event_history": [
|
||||
{
|
||||
"iso_datetime":
|
||||
"2020-12-29T00:35:00+03:00",
|
||||
"new_status": "created"
|
||||
}, {
|
||||
"iso_datetime":
|
||||
"2020-12-29T00:35:10+03:00",
|
||||
"new_status": "payment_approved"
|
||||
}, {
|
||||
"iso_datetime":
|
||||
"2020-12-29T00:35:20+03:00",
|
||||
"new_status": "preparing_started"
|
||||
}, {
|
||||
"iso_datetime":
|
||||
"2020-12-29T00:35:30+03:00",
|
||||
"new_status": "ready"
|
||||
}
|
||||
]}
|
||||
```
|
||||
|
||||
Suppose at some moment we decided to allow trustworthy clients to get their coffee in advance before the payment is confirmed. So an order will jump straight to "preparing_started", or event "ready", without a "payment_approved" event being emitted. It might appear to you that this modification *is* backwards-compatible since you've never really promised any specific event order being maintained, but it is not.
|
||||
|
@ -59,9 +59,15 @@ To make things worse, let us state that the inverse principle is actually correc
|
||||
We have already found a localization context. There is some set of languages and regions we support in our API, and there are requirements — what exactly the partner must provide to make our API work in a new region. More specifically, there must be some formatting function to represent beverage volume somewhere in our API code:
|
||||
|
||||
```
|
||||
l10n.volume.format(value, language_code, country_code)
|
||||
// l10n.formatVolume('300ml', 'en', 'UK') → '300 ml'
|
||||
// l10n.formatVolume('300ml', 'en', 'US') → '10 fl oz'
|
||||
l10n.volume.format(
|
||||
value, language_code, country_code
|
||||
)
|
||||
// l10n.formatVolume(
|
||||
// '300ml', 'en', 'UK'
|
||||
// ) → '300 ml'
|
||||
// l10n.formatVolume(
|
||||
// '300ml', 'en', 'US'
|
||||
// ) → '10 fl oz'
|
||||
```
|
||||
|
||||
To make our API work correctly with a new language or region, the partner must either define this function or point which pre-existing implementation to use. Like this:
|
||||
|
@ -5,7 +5,7 @@ Regretfully, many API providers pay miserable attention to the documentation qua
|
||||
Before we start describing documentation types and formats, we should stress one important statement: developers interact with your help articles totally unlike you expect them to. Remember yourself working on the project: you make quite specific actions.
|
||||
|
||||
1. First, you need to determine whether this service covers your needs in general (as quickly as possible);
|
||||
2. If yes, you look for specific functionality to resolve your specific case.
|
||||
2. If it does, you look for specific functionality to resolve your specific case.
|
||||
|
||||
In fact, newcomers (e.g. those developers who are not familiar with the API) usually want just one thing: to assemble the code that solves their problem out of existing code samples and never return to this issue again. Sounds not exactly reassuringly, given the amount of work invested into the API and its documentation development, but that's what the reality looks like. Also, that's the root cause of developers' dissatisfaction with the docs: it's literally impossible to have articles covering exactly that problem the developer comes with being detailed exactly to the extent the developer knows the API concepts. In addition, non-newcomers (e.g. those developers who have already learned the basics concepts and are now trying to solve some advanced problems) do not need these ‘mixed examples’ articles as they look for some deeper understanding.
|
||||
|
||||
|
@ -110,7 +110,8 @@ POST /v1/orders
|
||||
|
||||
* Машины с предустановленными программами:
|
||||
```
|
||||
// Возвращает список предустановленных программ
|
||||
// Возвращает список
|
||||
// предустановленных программ
|
||||
GET /programs
|
||||
→
|
||||
{
|
||||
@ -121,7 +122,8 @@ POST /v1/orders
|
||||
}
|
||||
```
|
||||
```
|
||||
// Запускает указанную программу на исполнение
|
||||
// Запускает указанную
|
||||
// программу на исполнение
|
||||
// и возвращает статус исполнения
|
||||
POST /execute
|
||||
{
|
||||
@ -132,7 +134,8 @@ POST /v1/orders
|
||||
{
|
||||
// Уникальный идентификатор задания
|
||||
"execution_id": "01-01",
|
||||
// Идентификатор исполняемой программы
|
||||
// Идентификатор
|
||||
// исполняемой программы
|
||||
"program": 1,
|
||||
// Запрошенный объём напитка
|
||||
"volume": "200ml"
|
||||
@ -144,7 +147,8 @@ POST /v1/orders
|
||||
```
|
||||
```
|
||||
// Возвращает статус исполнения
|
||||
// Формат аналогичен формату ответа `POST /execute`
|
||||
// Формат аналогичен
|
||||
// формату ответа `POST /execute`
|
||||
GET /execution/status
|
||||
```
|
||||
|
||||
@ -152,7 +156,8 @@ POST /v1/orders
|
||||
|
||||
* Машины с предустановленными функциями:
|
||||
```
|
||||
// Возвращает список доступных функций
|
||||
// Возвращает список
|
||||
// доступных функций
|
||||
GET /functions
|
||||
→
|
||||
{
|
||||
@ -164,9 +169,13 @@ POST /v1/orders
|
||||
// * pour_water — пролить воду
|
||||
// * discard_cup — утилизировать стакан
|
||||
"type": "set_cup",
|
||||
// Допустимые аргументы для каждой операции
|
||||
// Для простоты ограничимся одним аргументом:
|
||||
// * volume — объём стакана, кофе или воды
|
||||
// Допустимые аргументы
|
||||
// для каждой операции
|
||||
// Для простоты ограничимся
|
||||
// одним аргументом:
|
||||
// * volume
|
||||
// — объём стакана,
|
||||
// кофе или воды
|
||||
"arguments": ["volume"]
|
||||
},
|
||||
…
|
||||
@ -175,11 +184,15 @@ POST /v1/orders
|
||||
```
|
||||
```
|
||||
// Запускает на исполнение функцию
|
||||
// с передачей указанных значений аргументов
|
||||
// с передачей указанных
|
||||
// значений аргументов
|
||||
POST /functions
|
||||
{
|
||||
"type": "set_cup",
|
||||
"arguments": [{ "name": "volume", "value": "300ml" }]
|
||||
"arguments": [{
|
||||
"name": "volume",
|
||||
"value": "300ml"
|
||||
}]
|
||||
}
|
||||
```
|
||||
```
|
||||
@ -190,9 +203,12 @@ POST /v1/orders
|
||||
"sensors": [
|
||||
{
|
||||
// Допустимые значения
|
||||
// * cup_volume — объём установленного стакана
|
||||
// * ground_coffee_volume — объём смолотого кофе
|
||||
// * cup_filled_volume — объём напитка в стакане
|
||||
// * cup_volume
|
||||
// — объём установленного стакана
|
||||
// * ground_coffee_volume
|
||||
// — объём смолотого кофе
|
||||
// * cup_filled_volume
|
||||
// — объём напитка в стакане
|
||||
"type": "cup_volume",
|
||||
"value": "200ml"
|
||||
},
|
||||
@ -264,7 +280,7 @@ POST /v1/programs/{id}/run
|
||||
|
||||
Обратите внимание, что во всей этой цепочке вообще никак не участвует тип API кофемашины — собственно, ровно для этого мы и абстрагировали. Мы могли бы сделать интерфейсы более конкретными, разделив функциональность `run` и `match` для разных API, т.е. ввести раздельные endpoint-ы:
|
||||
* `POST /v1/program-matcher/{api_type}`
|
||||
* `POST /v1/programs/{api_type}/{program_id}/run`
|
||||
* `POST /v1/{api_type}/programs/{id}/run`
|
||||
|
||||
Достоинством такого подхода была бы возможность передавать в match и run не унифицированные наборы параметров, а только те, которые имеют значение в контексте указанного типа API. Однако в нашем дизайне API такой необходимости не прослеживается. Обработчик `run` сам может извлечь нужные параметры из мета-информации о программе и выполнить одно из двух действий:
|
||||
* вызвать `POST /execute` физического API кофемашины с передачей внутреннего идентификатора программы — для машин, поддерживающих API первого типа;
|
||||
@ -274,7 +290,11 @@ POST /v1/programs/{id}/run
|
||||
|
||||
```
|
||||
POST /v1/runtimes
|
||||
{ "coffee_machine", "program", "parameters" }
|
||||
{
|
||||
"coffee_machine",
|
||||
"program",
|
||||
"parameters"
|
||||
}
|
||||
→
|
||||
{ "runtime_id", "state" }
|
||||
```
|
||||
@ -302,15 +322,20 @@ POST /v1/runtimes
|
||||
// * "ready_waiting" — напиток готов
|
||||
// * "finished" — все операции завершены
|
||||
"status": "ready_waiting",
|
||||
// Текущая исполняемая команда (необязательное)
|
||||
// Текущая исполняемая команда
|
||||
// (необязательное)
|
||||
"command_sequence_id",
|
||||
// Чем закончилось исполнение программы
|
||||
// (необязательное)
|
||||
// * "success" — напиток приготовлен и выдан
|
||||
// * "terminated" — исполнение остановлено
|
||||
// * "technical_error" — ошибка при приготовлении
|
||||
// * "waiting_time_exceeded" — готовый заказ был
|
||||
// утилизирован, т.к. его не забрали
|
||||
// * "success"
|
||||
// — напиток приготовлен и выдан
|
||||
// * "terminated"
|
||||
// — исполнение остановлено
|
||||
// * "technical_error"
|
||||
// — ошибка при приготовлении
|
||||
// * "waiting_time_exceeded"
|
||||
// — готовый заказ был
|
||||
// утилизирован, его не забрали
|
||||
"resolution": "success",
|
||||
// Значения всех переменных,
|
||||
// включая состояние сенсоров
|
||||
|
@ -49,17 +49,23 @@
|
||||
В псевдокоде это будет выглядеть примерно вот так:
|
||||
```
|
||||
// Получить все доступные рецепты
|
||||
let recipes = api.getRecipes();
|
||||
let recipes =
|
||||
api.getRecipes();
|
||||
// Получить все доступные кофемашины
|
||||
let coffeeMachines = api.getCoffeeMachines();
|
||||
let coffeeMachines =
|
||||
api.getCoffeeMachines();
|
||||
// Построить пространственный индекс
|
||||
let coffeeMachineRecipesIndex = buildGeoIndex(recipes, coffeeMachines);
|
||||
// Выбрать кофемашины, соответствующие запросу пользователя
|
||||
let matchingCoffeeMachines = coffeeMachineRecipesIndex.query(
|
||||
parameters,
|
||||
{ "sort_by": "distance" }
|
||||
);
|
||||
// Наконец, показать предложения пользователю
|
||||
let coffeeMachineRecipesIndex =
|
||||
buildGeoIndex(recipes, coffeeMachines);
|
||||
// Выбрать кофемашины,
|
||||
// соответствующие запросу пользователя
|
||||
let matchingCoffeeMachines =
|
||||
coffeeMachineRecipesIndex.query(
|
||||
parameters,
|
||||
{ "sort_by": "distance" }
|
||||
);
|
||||
// Наконец, показать
|
||||
// предложения пользователю
|
||||
app.display(coffeeMachines);
|
||||
```
|
||||
|
||||
@ -83,9 +89,12 @@ POST /v1/offers/search
|
||||
}
|
||||
→
|
||||
{
|
||||
"results": [
|
||||
{ "coffee_machine", "place", "distance", "offer" }
|
||||
],
|
||||
"results": [{
|
||||
"coffee_machine",
|
||||
"place",
|
||||
"distance",
|
||||
"offer"
|
||||
}],
|
||||
"cursor"
|
||||
}
|
||||
```
|
||||
@ -121,12 +130,15 @@ app.display(offers);
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"coffee_machine", "place", "distance",
|
||||
"coffee_machine",
|
||||
"place",
|
||||
"distance",
|
||||
"offer": {
|
||||
"id",
|
||||
"price",
|
||||
"currency_code",
|
||||
// Указываем дату и время, до наступления которых
|
||||
// Указываем дату и время,
|
||||
// до наступления которых
|
||||
// предложение является актуальным
|
||||
"valid_until"
|
||||
}
|
||||
@ -175,10 +187,12 @@ POST /v1/orders
|
||||
// Род ошибки
|
||||
"reason": "offer_invalid",
|
||||
"localized_message":
|
||||
"Что-то пошло не так. Попробуйте перезагрузить приложение."
|
||||
"Что-то пошло не так.⮠
|
||||
Попробуйте перезагрузить приложение."
|
||||
"details": {
|
||||
// Что конкретно неправильно?
|
||||
// Какие из проверок валидности предложения
|
||||
// Какие из проверок
|
||||
// валидности предложения
|
||||
// отработали с ошибкой?
|
||||
"checks_failed": [
|
||||
"offer_lifetime"
|
||||
@ -202,45 +216,43 @@ POST /v1/orders
|
||||
Рассмотрим простой пример: что должна возвращать функция поиска подходящей кофемашины. Для обеспечения хорошего UX приложения необходимо передать довольно значительные объёмы информации.
|
||||
```
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"coffee_machine_id",
|
||||
// Тип кофемашины
|
||||
"coffee_machine_type": "drip_coffee_maker",
|
||||
// Марка кофемашины
|
||||
"coffee_machine_brand",
|
||||
// Название заведения
|
||||
"place_name": "Кафе «Ромашка»",
|
||||
// Координаты
|
||||
"place_location_latitude",
|
||||
"place_location_longitude",
|
||||
// Флаг «открыто сейчас»
|
||||
"place_open_now",
|
||||
// Часы работы
|
||||
"working_hours",
|
||||
// Сколько идти: время и расстояние
|
||||
"walking_distance",
|
||||
"walking_time",
|
||||
// Как найти заведение и кофемашину
|
||||
"place_location_tip",
|
||||
"offers": [
|
||||
{
|
||||
"recipe": "lungo",
|
||||
"recipe_name": "Наш фирменный лунго®™",
|
||||
"recipe_description",
|
||||
"volume": "800ml",
|
||||
"offer_id",
|
||||
"offer_valid_until",
|
||||
"localized_price": "Большая чашка всего за 19 баксов",
|
||||
"price": "19.00",
|
||||
"currency_code": "USD",
|
||||
"estimated_waiting_time": "20s"
|
||||
},
|
||||
…
|
||||
]
|
||||
},
|
||||
…
|
||||
]
|
||||
"results": [{
|
||||
"coffee_machine_id",
|
||||
// Тип кофемашины
|
||||
"coffee_machine_type":
|
||||
"drip_coffee_maker",
|
||||
// Марка кофемашины
|
||||
"coffee_machine_brand",
|
||||
// Название заведения
|
||||
"place_name": "Кафе «Ромашка»",
|
||||
// Координаты
|
||||
"place_location_latitude",
|
||||
"place_location_longitude",
|
||||
// Флаг «открыто сейчас»
|
||||
"place_open_now",
|
||||
// Часы работы
|
||||
"working_hours",
|
||||
// Сколько идти: время и расстояние
|
||||
"walking_distance",
|
||||
"walking_time",
|
||||
// Как найти заведение и кофемашину
|
||||
"place_location_tip",
|
||||
"offers": [{
|
||||
"recipe": "lungo",
|
||||
"recipe_name":
|
||||
"Наш фирменный лунго®™",
|
||||
"recipe_description",
|
||||
"volume": "800ml",
|
||||
"offer_id",
|
||||
"offer_valid_until",
|
||||
"localized_price":
|
||||
"Большая чашка⮠
|
||||
всего за 19 баксов",
|
||||
"price": "19.00",
|
||||
"currency_code": "USD",
|
||||
"estimated_waiting_time": "20s"
|
||||
}, …]
|
||||
}, …]
|
||||
}
|
||||
```
|
||||
|
||||
@ -260,22 +272,36 @@ POST /v1/orders
|
||||
{
|
||||
"results": [{
|
||||
// Данные о заведении
|
||||
"place": { "name", "location" },
|
||||
"place":
|
||||
{ "name", "location" },
|
||||
// Данные о кофемашине
|
||||
"coffee-machine": { "id", "brand", "type" },
|
||||
"coffee-machine":
|
||||
{ "id", "brand", "type" },
|
||||
// Как добраться
|
||||
"route": { "distance", "duration", "location_tip" },
|
||||
"route": {
|
||||
"distance",
|
||||
"duration",
|
||||
"location_tip"
|
||||
},
|
||||
// Предложения напитков
|
||||
"offers": [{
|
||||
// Рецепт
|
||||
"recipe": { "id", "name", "description" },
|
||||
"recipe":
|
||||
{ "id", "name", "description" },
|
||||
// Данные относительно того,
|
||||
// как рецепт готовят на конкретной кофемашине
|
||||
"options": { "volume" },
|
||||
// как рецепт готовят
|
||||
// на конкретной кофемашине
|
||||
"options":
|
||||
{ "volume" },
|
||||
// Метаданные предложения
|
||||
"offer": { "id", "valid_until" },
|
||||
"offer":
|
||||
{ "id", "valid_until" },
|
||||
// Цена
|
||||
"pricing": { "currency_code", "price", "localized_price" },
|
||||
"pricing": {
|
||||
"currency_code",
|
||||
"price",
|
||||
"localized_price"
|
||||
},
|
||||
"estimated_waiting_time"
|
||||
}, …]
|
||||
}, …]
|
||||
|
@ -117,8 +117,8 @@ strpbrk (str1, str2)
|
||||
**Хорошо**:
|
||||
```
|
||||
str_search_for_characters(
|
||||
lookup_character_set,
|
||||
str
|
||||
str,
|
||||
lookup_character_set
|
||||
)
|
||||
```
|
||||
— однако необходимость существования такого метода вообще вызывает сомнения, достаточно было бы иметь удобную функцию поиска подстроки с нужными параметрами. Аналогично сокращение `string` до `str` выглядит совершенно бессмысленным, но, увы, является устоявшимся для большого количества предметных областей.
|
||||
@ -256,27 +256,27 @@ POST /v1/orders
|
||||
**Плохо**:
|
||||
```
|
||||
// Создаёт пользователя
|
||||
POST /users
|
||||
POST /v1/users
|
||||
{ … }
|
||||
→
|
||||
// Пользователи создаются по умолчанию
|
||||
// с указанием лимита трат в месяц
|
||||
{
|
||||
"spending_monthly_limit_usd": "100",
|
||||
…
|
||||
"spending_monthly_limit_usd": "100"
|
||||
}
|
||||
// Для отмены лимита требуется
|
||||
// указать значение null
|
||||
POST /users
|
||||
PUT /v1/users/{id}
|
||||
{
|
||||
"spending_monthly_limit_usd": null,
|
||||
…
|
||||
"spending_monthly_limit_usd": null
|
||||
}
|
||||
```
|
||||
|
||||
**Хорошо**
|
||||
```
|
||||
POST /users
|
||||
POST /v1/users
|
||||
{
|
||||
// true — у пользователя снят
|
||||
// лимит трат в месяц
|
||||
@ -299,7 +299,7 @@ POST /users
|
||||
|
||||
**Плохо**
|
||||
```
|
||||
POST /search
|
||||
POST /v1/coffee-machines/search
|
||||
{
|
||||
"query": "lungo",
|
||||
"location": <положение пользователя>
|
||||
@ -315,7 +315,7 @@ POST /search
|
||||
|
||||
**Хорошо**:
|
||||
```
|
||||
POST /search
|
||||
POST /v1/coffee-machines/search
|
||||
{
|
||||
"query": "lungo",
|
||||
"location": <положение пользователя>
|
||||
@ -1034,7 +1034,7 @@ PATCH /v1/recipes
|
||||
Если первые две проблемы решаются чисто техническими средствами (см. соответствующие разделы), то третья проблема скорее логическая: каким образом разумно организовать канал обновления состояния клиента так, чтобы найти баланс между отзывчивостью системы и затраченными на эту отзывчивость ресурсами. Здесь мы можем дать несколько рекомендаций:
|
||||
|
||||
* не злоупотребляйте асинхронными интерфейсами;
|
||||
* с одной стороны, они позволяют нивелировать многие технических проблем с производительностью API, что, в свою очередь, позволяет поддерживать обратную совместимость: если метод изначально асинхронный, то можно без проблем увеличивать время обработки и менять модель консистентности данных;
|
||||
* с одной стороны, они позволяют нивелировать многие технические проблемы с производительностью API, что, в свою очередь, позволяет поддерживать обратную совместимость: если метод изначально асинхронный, то можно без проблем увеличивать время обработки и менять модель консистентности данных;
|
||||
* с другой стороны, количество генерируемых клиентами запросов становится трудно предсказуемым, поскольку для получения результата клиенту необходимо сделать заранее неизвестное число обращений;
|
||||
|
||||
* объявляйте явную политику перезапросов (например, посредством заголовка `Retry-After`);
|
||||
@ -1042,7 +1042,7 @@ PATCH /v1/recipes
|
||||
|
||||
* если вы ожидаете значительного количества асинхронных операций в API, изначально дайте разработчику выбор между моделями poll (клиент самостоятельно производит новые запросы к API чтобы проверить, не изменился ли статус асинхронной операций) и push (сервер уведомляет клиентов об изменениях статусов посредством отправки специального запроса, например, через webhook-и или server push-механизмы);
|
||||
|
||||
* если в рамках одной сущности необходимо предоставлять как «лёгкие» (скажем, название и описание рецепта), так и «тяжёлые» данные (скажем, промо-фотография напитка, которая легко может по разумеру превышать текстовые поля в сотни раз), лучше разделить эндпойнты и отдавать только ссылку для доступа к «тяжёлым» данным (в нашем случае, ссылку на изображение) — это, как минимум, позволит задавать различные политики кэширования для разных данных.
|
||||
* если в рамках одной сущности необходимо предоставлять как «лёгкие» (скажем, название и описание рецепта), так и «тяжёлые» данные (скажем, промо-фотография напитка, которая легко может по размеру превышать текстовые поля в сотни раз), лучше разделить эндпойнты и отдавать только ссылку для доступа к «тяжёлым» данным (в нашем случае, ссылку на изображение) — это, как минимум, позволит задавать различные политики кэширования для разных данных.
|
||||
|
||||
Неплохим упражнением здесь будет промоделировать типовой жизненный цикл основной функциональности приложения партнёра (например, выполнение одного заказа) и подсчитать общее количество запросов и объём трафика на один цикл.
|
||||
|
||||
@ -1083,7 +1083,7 @@ PATCH /v1/orders/{id}
|
||||
|
||||
Эта сигнатура плоха сама по себе, поскольку является нечитабельной. Что обозначает пустой первый элемент массива — это удаление элемента или указание на отсутствие изменений? Что произойдёт с полями, которые не указаны в операции обновления (`delivery_address`, `milk_type`) — они будут сброшены в значения по умолчанию или останутся неизменными?
|
||||
|
||||
Самое неприятное здесь — какой бы вариант вы ни выбрали, это только начало проблем. Допустим, мы договорились, что конструкция `{ "items":[null, {…}] }` означает, что с первым элементом массива ничего не происходит, он не меняется. А как тогда всё-таки его удалить? Придумать ещё одно «зануляемое» значение специально для удаления? Аналогично, если значения неуказанных полей остаются без изменений — как сбросить их в значения по умолчанию?
|
||||
Самое неприятное здесь — какой бы вариант вы ни выбрали, это только начало проблем. Допустим, мы договорились, что конструкция `{"items":[null, {…}]}` означает, что с первым элементом массива ничего не происходит, он не меняется. А как тогда всё-таки его удалить? Придумать ещё одно «зануляемое» значение специально для удаления? Аналогично, если значения неуказанных полей остаются без изменений — как сбросить их в значения по умолчанию?
|
||||
|
||||
**Простое решение** состоит в том, чтобы всегда перезаписывать объект целиком, т.е. требовать передачи полного объекта, полностью заменять им текущее состояние и возвращать в ответ на операцию новое состояние целиком. Однако это простое решение часто не принимается по нескольким причинам:
|
||||
* повышенные размеры запросов и, как следствие, расход трафика;
|
||||
|
@ -18,22 +18,36 @@ POST /v1/offers/search
|
||||
{
|
||||
"results": [{
|
||||
// Данные о заведении
|
||||
"place": { "name", "location" },
|
||||
"place":
|
||||
{ "name", "location" },
|
||||
// Данные о кофемашине
|
||||
"coffee_machine": { "id", "brand", "type" },
|
||||
"coffee_machine":
|
||||
{ "id", "brand", "type" },
|
||||
// Как добраться
|
||||
"route": { "distance", "duration", "location_tip" },
|
||||
"route": {
|
||||
"distance",
|
||||
"duration",
|
||||
"location_tip"
|
||||
},
|
||||
// Предложения напитков
|
||||
"offers": [{
|
||||
// Рецепт
|
||||
"recipe": { "id", "name", "description" },
|
||||
"recipe":
|
||||
{ "id", "name", "description" },
|
||||
// Данные относительно того,
|
||||
// как рецепт готовят на конкретной кофемашине
|
||||
"options": { "volume" },
|
||||
// как рецепт готовят
|
||||
// на конкретной кофемашине
|
||||
"options":
|
||||
{ "volume" },
|
||||
// Метаданные предложения
|
||||
"offer": { "id", "valid_until" },
|
||||
"offer":
|
||||
{ "id", "valid_until" },
|
||||
// Цена
|
||||
"pricing": { "currency_code", "price", "localized_price" },
|
||||
"pricing": {
|
||||
"currency_code",
|
||||
"price",
|
||||
"localized_price"
|
||||
},
|
||||
"estimated_waiting_time"
|
||||
}, …]
|
||||
}, …]
|
||||
@ -41,7 +55,6 @@ POST /v1/offers/search
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
##### Работа с рецептами
|
||||
```
|
||||
// Возвращает список рецептов
|
||||
@ -55,7 +68,11 @@ GET /v1/recipes?cursor=<курсор>
|
||||
// по его идентификатору
|
||||
GET /v1/recipes/{id}
|
||||
→
|
||||
{ "recipe_id", "name", "description" }
|
||||
{
|
||||
"recipe_id",
|
||||
"name",
|
||||
"description"
|
||||
}
|
||||
```
|
||||
##### Работа с заказами
|
||||
```
|
||||
@ -140,7 +157,11 @@ POST /v1/runs/{id}/cancel
|
||||
```
|
||||
// Создаёт новый рантайм
|
||||
POST /v1/runtimes
|
||||
{ "coffee_machine_id", "program_id", "parameters" }
|
||||
{
|
||||
"coffee_machine_id",
|
||||
"program_id",
|
||||
"parameters"
|
||||
}
|
||||
→
|
||||
{ "runtime_id", "state" }
|
||||
```
|
||||
@ -150,7 +171,8 @@ POST /v1/runtimes
|
||||
GET /v1/runtimes/{runtime_id}/state
|
||||
{
|
||||
"status": "ready_waiting",
|
||||
// Текущая исполняемая команда (необязательное)
|
||||
// Текущая исполняемая команда
|
||||
// (необязательное)
|
||||
"command_sequence_id",
|
||||
"resolution": "success",
|
||||
"variables"
|
||||
|
@ -48,7 +48,8 @@ while (true) {
|
||||
try {
|
||||
status = api.getStatus(order.id);
|
||||
} catch (e) {
|
||||
if (e.httpStatusCode != 404 || timeoutExceeded()) {
|
||||
if (e.httpStatusCode != 404 ||
|
||||
timeoutExceeded()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -99,26 +100,25 @@ object.observe('widthchange', observerFunction);
|
||||
```
|
||||
GET /v1/orders/{id}/events/history
|
||||
→
|
||||
{
|
||||
"event_history": [
|
||||
{
|
||||
"iso_datetime": "2020-12-29T00:35:00+03:00",
|
||||
"new_status": "created"
|
||||
},
|
||||
{
|
||||
"iso_datetime": "2020-12-29T00:35:10+03:00",
|
||||
"new_status": "payment_approved"
|
||||
},
|
||||
{
|
||||
"iso_datetime": "2020-12-29T00:35:20+03:00",
|
||||
"new_status": "preparing_started"
|
||||
},
|
||||
{
|
||||
"iso_datetime": "2020-12-29T00:35:30+03:00",
|
||||
"new_status": "ready"
|
||||
}
|
||||
]
|
||||
}
|
||||
{ "event_history": [
|
||||
{
|
||||
"iso_datetime":
|
||||
"2020-12-29T00:35:00+03:00",
|
||||
"new_status": "created"
|
||||
}, {
|
||||
"iso_datetime":
|
||||
"2020-12-29T00:35:10+03:00",
|
||||
"new_status": "payment_approved"
|
||||
}, {
|
||||
"iso_datetime":
|
||||
"2020-12-29T00:35:20+03:00",
|
||||
"new_status": "preparing_started"
|
||||
}, {
|
||||
"iso_datetime":
|
||||
"2020-12-29T00:35:30+03:00",
|
||||
"new_status": "ready"
|
||||
}
|
||||
]}
|
||||
```
|
||||
|
||||
Допустим, в какой-то момент вы решили надёжным клиентам с хорошей историей заказов предоставлять кофе «в кредит», не дожидаясь подтверждения платежа. Т.е. заказ перейдёт в статус `"preparing_started"`, а может и `"ready"`, вообще без события `"payment_approved"`. Вам может показаться, что это изменение является обратно-совместимым — в самом деле, вы же и не обещали никакого конкретного порядка событий. Но это, конечно, не так.
|
||||
|
@ -64,7 +64,8 @@ PUT /v1/partners/{partnerId}/coffee-machines
|
||||
|
||||
2. Добавляем новый метод `with-options`:
|
||||
```
|
||||
PUT /v1/partners/{partner_id}/coffee-machines-with-options
|
||||
PUT /v1/partners/{partner_id}⮠
|
||||
/coffee-machines-with-options
|
||||
{
|
||||
"coffee_machines": [{
|
||||
"id",
|
||||
|
@ -59,9 +59,15 @@ POST /v1/recipes
|
||||
Как уже понятно, существует контекст локализации. Есть какой-то набор языков и регионов, которые мы поддерживаем в нашем API, и есть требования — что конкретно необходимо предоставить партнёру, чтобы API заработал на новом языке в новом регионе. Конкретно в случае объёма кофе где-то в недрах нашего API есть функция форматирования строк для отображения объёма напитка:
|
||||
|
||||
```
|
||||
l10n.volume.format(value, language_code, country_code)
|
||||
// l10n.formatVolume('300ml', 'en', 'UK') → '300 ml'
|
||||
// l10n.formatVolume('300ml', 'en', 'US') → '10 fl oz'
|
||||
l10n.volume.format(
|
||||
value, language_code, country_code
|
||||
)
|
||||
// l10n.formatVolume(
|
||||
// '300ml', 'en', 'UK'
|
||||
// ) → '300 ml'
|
||||
// l10n.formatVolume(
|
||||
// '300ml', 'en', 'US'
|
||||
// ) → '10 fl oz'
|
||||
```
|
||||
|
||||
Чтобы наш API корректно заработал с новым языком или регионом, партнёр должен или задать эту функцию, или указать, какую из существующих локализаций необходимо использовать. Для этого мы абстрагируем-и-расширяем API, в соответствии с описанной в предыдущей главе процедурой, и добавляем новый эндпойнт — настройки форматирования:
|
||||
@ -95,7 +101,8 @@ PUT /formatters/volume/ru/US
|
||||
GET /v1/layouts/{layout_id}
|
||||
{
|
||||
"id",
|
||||
// Макетов вполне возможно будет много разных,
|
||||
// Макетов вполне возможно
|
||||
// будет много разных,
|
||||
// поэтому имеет смысл сразу заложить
|
||||
// расширяемость
|
||||
"kind": "recipe_search",
|
||||
@ -106,7 +113,8 @@ GET /v1/layouts/{layout_id}
|
||||
// Раз уж мы договорились, что `name`
|
||||
// на самом деле нужен как заголовок
|
||||
// в списке результатов поиска —
|
||||
// разумнее его так и назвать `search_title`
|
||||
// разумнее его так и назвать
|
||||
// `search_title`
|
||||
"field": "search_title",
|
||||
"view": {
|
||||
// Машиночитаемое описание того,
|
||||
@ -117,14 +125,18 @@ GET /v1/layouts/{layout_id}
|
||||
}
|
||||
}, …],
|
||||
// Какие поля обязательны
|
||||
"required": ["search_title", "search_description"]
|
||||
"required": [
|
||||
"search_title",
|
||||
"search_description"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Таким образом, партнёр сможет сам решить, какой вариант ему предпочтителен. Можно задать необходимые поля для стандартного макета:
|
||||
|
||||
```
|
||||
PUT /v1/recipes/{id}/properties/l10n/{lang}
|
||||
PUT /v1/recipes/{id}/⮠
|
||||
properties/l10n/{lang}
|
||||
{
|
||||
"search_title", "search_description"
|
||||
}
|
||||
@ -170,8 +182,14 @@ POST /v1/recipe-builder
|
||||
// Добавляем нужные форматтеры
|
||||
"formatters": {
|
||||
"volume": [
|
||||
{ "language_code", "template" },
|
||||
{ "language_code", "country_code", "template" }
|
||||
{
|
||||
"language_code",
|
||||
"template"
|
||||
}, {
|
||||
"language_code",
|
||||
"country_code",
|
||||
"template"
|
||||
}
|
||||
]
|
||||
},
|
||||
// Прочие действия, которые необходимо
|
||||
@ -194,7 +212,8 @@ POST /v1/recipes/custom
|
||||
}
|
||||
→
|
||||
{
|
||||
"id": "my-coffee-company:lungo-customato"
|
||||
"id":
|
||||
"my-coffee-company:lungo-customato"
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -88,24 +88,33 @@ PUT /v1/api-types/{api_type}
|
||||
```
|
||||
/* Имплементация партнёром интерфейса
|
||||
запуска программы на его кофемашинах */
|
||||
registerProgramRunHandler(apiType, (context) => {
|
||||
// Инициализируем запуск исполнения
|
||||
// программы на стороне партнёра
|
||||
let execution = initExecution(context, …);
|
||||
// Подписываемся на события
|
||||
// изменения контекста
|
||||
context.on('takeout_requested', () => {
|
||||
// Если запрошена выдача напитка,
|
||||
// инициализируем выдачу
|
||||
execution.prepareTakeout(() => {
|
||||
// как только напиток готов к выдаче,
|
||||
// сигнализируем об этом
|
||||
execution.context.emit('takeout_ready');
|
||||
});
|
||||
});
|
||||
registerProgramRunHandler(
|
||||
apiType,
|
||||
(context) => {
|
||||
// Инициализируем запуск исполнения
|
||||
// программы на стороне партнёра
|
||||
let execution =
|
||||
initExecution(context, …);
|
||||
// Подписываемся на события
|
||||
// изменения контекста
|
||||
context.on(
|
||||
'takeout_requested',
|
||||
() => {
|
||||
// Если запрошена выдача напитка,
|
||||
// инициализируем выдачу
|
||||
execution.prepareTakeout(() => {
|
||||
// как только напиток
|
||||
// готов к выдаче,
|
||||
// сигнализируем об этом
|
||||
execution.context
|
||||
.emit('takeout_ready');
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
return execution.context;
|
||||
});
|
||||
return execution.context;
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
**NB**: в случае HTTP API соответствующий пример будет выглядеть более громоздко, поскольку потребует создания отдельных эндпойнтов чтения очередей событий типа `GET /program-run/events` и `GET /partner/{id}/execution/events`, это упражнение мы оставляем читателю. Следует также отметить, что в реальных системах потоки событий часто направляют через внешнюю шину типа Apache Kafka или Amazon SNS/SQS.
|
||||
@ -134,26 +143,37 @@ registerProgramRunHandler(apiType, (context) => {
|
||||
```
|
||||
/* Имплементация партнёром интерфейса
|
||||
запуска программы на его кофемашинах */
|
||||
registerProgramRunHandler(apiType, (context) => {
|
||||
// Инициализируем запуск исполнения
|
||||
// программы на стороне партнёра
|
||||
let execution = initExecution(context, …);
|
||||
// Подписываемся на события
|
||||
// изменения контекста
|
||||
context.on('takeout_requested', () => {
|
||||
// Если запрошена выдача напитка,
|
||||
// инициализируем выдачу
|
||||
execution.prepareTakeout(() => {
|
||||
/* как только напиток готов к выдаче,
|
||||
сигнализируем об этом, но не
|
||||
посредством генерации события */
|
||||
// execution.context.emit('takeout_ready')
|
||||
context.set('takeout_ready');
|
||||
// Или ещё более жёстко:
|
||||
// context.setTakeoutReady();
|
||||
});
|
||||
});
|
||||
// Так как мы сами изменяем родительский контекст
|
||||
registerProgramRunHandler(
|
||||
apiType,
|
||||
(context) => {
|
||||
// Инициализируем запуск исполнения
|
||||
// программы на стороне партнёра
|
||||
let execution =
|
||||
initExecution(context, …);
|
||||
// Подписываемся на события
|
||||
// изменения контекста
|
||||
context.on(
|
||||
'takeout_requested',
|
||||
() => {
|
||||
// Если запрошена выдача напитка,
|
||||
// инициализируем выдачу
|
||||
execution.prepareTakeout(() => {
|
||||
/* как только напиток
|
||||
готов к выдаче,
|
||||
сигнализируем об этом,
|
||||
вызовом метода контекста,
|
||||
а не генерацией события */
|
||||
// execution.context
|
||||
// .emit('takeout_ready')
|
||||
context.set('takeout_ready');
|
||||
// Или ещё более жёстко:
|
||||
// context.setTakeoutReady();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
// Так как мы сами
|
||||
// изменяем родительский контекст
|
||||
// нет нужды что-либо возвращать
|
||||
// return execution.context;
|
||||
}
|
||||
|
Reference in New Issue
Block a user