mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-03-17 20:42:26 +02:00
Приложение к разделу I и соответствующие правки
This commit is contained in:
parent
80e929ef1a
commit
8f31fa11b9
@ -240,7 +240,7 @@ POST /v1/orders
|
||||
|
||||
Имплементация функции `POST /orders` проверит все параметры заказа, заблокирует его стоимость на карте пользователя, сформирует полный запрос на исполнение и обратится к уровню исполнения. Сначала необходимо подобрать правильную программу исполнения:
|
||||
```
|
||||
POST /v1/programs/match
|
||||
POST /v1/program-matcher
|
||||
{ "recipe", "coffee-machine" }
|
||||
→
|
||||
{ "program_id" }
|
||||
@ -263,7 +263,7 @@ POST /v1/programs/{id}/run
|
||||
```
|
||||
|
||||
Обратите внимание, что во всей этой цепочке вообще никак не участвует тип API кофе-машины — собственно, ровно для этого мы и абстрагировали. Мы могли бы сделать интерфейсы более конкретными, разделив функциональность `run` и `match` для разных API, т.е. ввести раздельные endpoint-ы:
|
||||
* `POST /v1/programs/{api_type}/match`
|
||||
* `POST /v1/program-matcher/{api_type}`
|
||||
* `POST /v1/programs/{api_type}/{program_id}/run`
|
||||
|
||||
Достоинством такого подхода была бы возможность передавать в match и run не унифицированные наборы параметров, а только те, которые имеют значение в контексте указанного типа API. Однако в нашем дизайне API такой необходимости не прослеживается. Обработчик `run` сам может извлечь нужные параметры из мета-информации о программе и выполнить одно из двух действий:
|
||||
|
@ -43,7 +43,7 @@
|
||||
|
||||
Очевидно, первый шаг — нужно предоставить пользователю возможность выбора, чего он, собственно хочет. И первый же шаг обнажает неудобство использования нашего API: никаких методов, позволяющих пользователю что-то выбрать в нашем API нет. Разработчику придётся сделать что-то типа такого:
|
||||
* получить все доступные рецепты из `GET /v1/recipes`;
|
||||
* получить список всех кофе-машины из `GET /v1/coffee-machines`;
|
||||
* получить список всех кофе-машин из `GET /v1/coffee-machines`;
|
||||
* самостоятельно выбрать нужные данные.
|
||||
|
||||
В псевдокоде это будет выглядеть примерно вот так:
|
||||
@ -53,7 +53,7 @@ let recipes = api.getRecipes();
|
||||
// Получить все доступные кофе-машины
|
||||
let coffeeMachines = api.getCoffeeMachines();
|
||||
// Построить пространственный индекс
|
||||
let coffeeMachineRecipesIndex = buildGeoIndex(recipes, coffee-machines);
|
||||
let coffeeMachineRecipesIndex = buildGeoIndex(recipes, coffeeMachines);
|
||||
// Выбрать кофе-машины, соответствующие запросу пользователя
|
||||
let matchingCoffeeMachines = coffeeMachineRecipesIndex.query(
|
||||
parameters,
|
||||
@ -71,7 +71,7 @@ app.display(coffeeMachines);
|
||||
|
||||
Тогда наш новый интерфейс будет выглядеть примерно вот так:
|
||||
```
|
||||
POST /v1/coffee-machines/search
|
||||
POST /v1/offers/search
|
||||
{
|
||||
// опционально
|
||||
"recipes": ["lungo", "americano"],
|
||||
@ -94,19 +94,20 @@ POST /v1/coffee-machines/search
|
||||
* `offer` — некоторое «предложение»: на каких условиях можно заказать запрошенные виды кофе, если они были указаны, либо какое-то маркетинговое предложение — цены на самые популярные / интересные напитки, если пользователь не указал конкретные рецепты для поиска;
|
||||
* `place` — место (кафе, автомат, ресторан), где находится машина; мы не вводили эту сущность ранее, но, очевидно, пользователю потребуются какие-то более понятные ориентиры, нежели географические координаты, чтобы найти нужную кофе-машину.
|
||||
|
||||
**NB**. Мы могли бы не добавлять новый эндпойнт, а обогатить существующий `/coffee-machines`. Однако такое решение выглядит менее семантично: не стоит в рамках одного интерфейса смешивать способ перечисления объектов по порядку и по релевантности запросу, поскольку эти два вида ранжирования обладают существенно разными свойствами и сценариями использования.
|
||||
**NB**. Мы могли бы не добавлять новый эндпойнт, а обогатить существующий `/coffee-machines`. Однако такое решение выглядит менее семантично: не стоит в рамках одного интерфейса смешивать способ перечисления объектов по порядку и по релевантности запросу, поскольку эти два вида ранжирования обладают существенно разными свойствами и сценариями использования. К тому же, обогащение поиска «предложениями» скорее выводит эту функциональность из неймспейса «кофе-машины»: для пользователя всё-таки первичен факт получения предложения приготовить напиток на конкретных условиях, и кофе-машина — лишь одно из них. `/v1/offers/search` — более логичное имя для такого эндпойнта.
|
||||
|
||||
Вернёмся к коду, который напишет разработчик. Теперь он будет выглядеть примерно так:
|
||||
```
|
||||
// Ищем кофе-машины, соответствующие запросу пользователя
|
||||
let coffeeMachines = api.search(parameters);
|
||||
// Ищем предложения,
|
||||
// соответствующие запросу пользователя
|
||||
let offers = api.offerSearch(parameters);
|
||||
// Показываем пользователю
|
||||
app.display(coffeeMachines);
|
||||
app.display(offers);
|
||||
```
|
||||
|
||||
#### Хэлперы
|
||||
|
||||
Методы, подобные только что изобретённому нами `coffee-machines/search`, принято называть *хэлперами*. Цель их существования — обобщить понятные сценарии использования API и облегчить их. Под «облегчить» мы имеем в виду не только сократить многословность («бойлерплейт»), но и помочь разработчику избежать частых проблем и ошибок.
|
||||
Методы, подобные только что изобретённому нами `offers/search`, принято называть *хэлперами*. Цель их существования — обобщить понятные сценарии использования API и облегчить их. Под «облегчить» мы имеем в виду не только сократить многословность («бойлерплейт»), но и помочь разработчику избежать частых проблем и ошибок.
|
||||
|
||||
Рассмотрим, например, вопрос стоимости заказа. Наша функция поиска возвращает какие-то «предложения» с ценой. Но ведь цена может меняться: в «счастливый час» кофе может стоить меньше. Разработчик может ошибиться в имплементации этой функциональности трижды:
|
||||
* кэшировать на клиентском устройстве результаты поиска слишком долго (в результате цена всегда будет неактуальна),
|
||||
@ -136,7 +137,7 @@ app.display(coffeeMachines);
|
||||
```
|
||||
Поступая так, мы не только помогаем разработчику понять, когда ему надо обновить цены, но и решаем UX-задачу: как показать пользователю, что «счастливый час» скоро закончится. Идентификатор предложения может при этом быть stateful (фактически, аналогом сессии пользователя) или stateless (если мы точно знаем, до какого времени действительна цены, мы может просто закодировать это время в идентификаторе).
|
||||
|
||||
Альтернативно, кстати, можно было бы разделить функциональность поиска по заданным параметрам и получения офферов, т.е. добавить эндпойнт, только актуализирующий цены в конкретных кофейнях.
|
||||
Альтернативно, кстати, можно было бы разделить функциональность поиска по заданным параметрам и получения предложений, т.е. добавить эндпойнт, только актуализирующий цены в конкретных кофейнях.
|
||||
|
||||
#### Обработка ошибок
|
||||
|
||||
@ -256,7 +257,7 @@ POST /v1/orders
|
||||
Попробуем сгруппировать:
|
||||
```
|
||||
{
|
||||
"results": {
|
||||
"results": [{
|
||||
// Данные о заведении
|
||||
"place": { "name", "location" },
|
||||
// Данные о кофе-машине
|
||||
@ -276,7 +277,7 @@ POST /v1/orders
|
||||
"pricing": { "currency_code", "price", "localized_price" },
|
||||
"estimated_waiting_time"
|
||||
}
|
||||
}
|
||||
}, …]
|
||||
}
|
||||
```
|
||||
Такое API читать и воспринимать гораздо удобнее, нежели сплошную простыню различных атрибутов. Более того, возможно, стоит на будущее сразу дополнительно сгруппировать, например, `place` и `route` в одну структуру `location`, или `offer` и `pricing` в одну более общую структуру.
|
||||
|
@ -655,7 +655,7 @@ PATCH /v1/recipes
|
||||
}
|
||||
```
|
||||
|
||||
По сути, для клиента всё произошло ожидаемым образом: изменения были внесены, и последний полученный ответ всегда корректен. Однако по сути состояние ресурса после первого запросе отличалось от состояния ресурса после второго запроса, что противоречит самому определению идемпотентности.
|
||||
По сути, для клиента всё произошло ожидаемым образом: изменения были внесены, и последний полученный ответ всегда корректен. Однако по сути состояние ресурса после первого запроса отличалось от состояния ресурса после второго запроса, что противоречит самому определению идемпотентности.
|
||||
|
||||
Более корректно было бы при получении повторного запроса с тем же токеном ничего не делать и возвращать ту же разбивку ошибок, что была дана на первый запрос — но для этого придётся её каким-то образом хранить в истории изменений.
|
||||
|
||||
|
162
src/ru/clean-copy/02-Раздел I. Проектирование API/06.md
Normal file
162
src/ru/clean-copy/02-Раздел I. Проектирование API/06.md
Normal file
@ -0,0 +1,162 @@
|
||||
### Приложение к разделу I. Модельное API
|
||||
|
||||
Суммируем текущее состояние нашего учебного API.
|
||||
|
||||
##### Поиск предложений
|
||||
```
|
||||
POST /v1/offers/search
|
||||
{
|
||||
// опционально
|
||||
"recipes": ["lungo", "americano"],
|
||||
"position": <географические координаты>,
|
||||
"sort_by": [
|
||||
{ "field": "distance" }
|
||||
],
|
||||
"limit": 10
|
||||
}
|
||||
→
|
||||
{
|
||||
"results": [{
|
||||
// Данные о заведении
|
||||
"place": { "name", "location" },
|
||||
// Данные о кофе-машине
|
||||
"coffee-machine": { "brand", "type" },
|
||||
// Как добраться
|
||||
"route": { "distance", "duration", "location_tip" },
|
||||
// Предложения напитков
|
||||
"offers": {
|
||||
// Рецепт
|
||||
"recipe": { "id", "name", "description" },
|
||||
// Данные относительно того,
|
||||
// как рецепт готовят на конкретной кофе-машине
|
||||
"options": { "volume" },
|
||||
// Метаданные предложения
|
||||
"offer": { "id", "valid_until" },
|
||||
// Цена
|
||||
"pricing": { "currency_code", "price", "localized_price" },
|
||||
"estimated_waiting_time"
|
||||
}
|
||||
}, …]
|
||||
"cursor"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
##### Работа с рецептами
|
||||
```
|
||||
// Возвращает список рецептов
|
||||
// Параметр cursor необязателен
|
||||
GET /v1/recipes?cursor=<курсор>
|
||||
→
|
||||
{ "recipes", "cursor" }
|
||||
```
|
||||
```
|
||||
// Возвращает конкретный рецепт
|
||||
// по его идентификатору
|
||||
GET /v1/recipes/{id}
|
||||
→
|
||||
{ "recipe_id", "name", "description" }
|
||||
```
|
||||
##### Работа с заказами
|
||||
```
|
||||
// Размещает заказ
|
||||
POST /v1/orders
|
||||
{
|
||||
"coffee_machine_id",
|
||||
"currency_code",
|
||||
"price",
|
||||
"recipe": "lungo",
|
||||
// Опционально
|
||||
"offer_id",
|
||||
// Опционально
|
||||
"volume": "800ml"
|
||||
}
|
||||
→
|
||||
{ "order_id" }
|
||||
```
|
||||
```
|
||||
// Возвращает состояние заказа
|
||||
GET /v1/orders/{id}
|
||||
→
|
||||
{ "order_id", "status" }
|
||||
```
|
||||
```
|
||||
// Отменяет заказ
|
||||
POST /v1/orders/{id}/cancel
|
||||
```
|
||||
##### Работа с программами
|
||||
```
|
||||
// Возвращает идентификатор программы,
|
||||
// соответствующей указанному рецепту
|
||||
// на указанной кофе-машине
|
||||
POST /v1/program-matcher
|
||||
{ "recipe", "coffee-machine" }
|
||||
→
|
||||
{ "program_id" }
|
||||
```
|
||||
```
|
||||
// Возвращает описание
|
||||
// программы по её идентификатору
|
||||
GET /v1/programs/{id}
|
||||
→
|
||||
{
|
||||
"program_id",
|
||||
"api_type",
|
||||
"commands": [
|
||||
{
|
||||
"sequence_id",
|
||||
"type": "set_cup",
|
||||
"parameters"
|
||||
},
|
||||
…
|
||||
]
|
||||
}
|
||||
```
|
||||
##### Исполнение программ
|
||||
```
|
||||
// Запускает исполнение программы
|
||||
// с указанным идентификатором
|
||||
// на указанной машине
|
||||
// с указанными параметрами
|
||||
POST /v1/programs/{id}/run
|
||||
{
|
||||
"order_id",
|
||||
"coffee_machine_id",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "volume",
|
||||
"value": "800ml"
|
||||
}
|
||||
]
|
||||
}
|
||||
→
|
||||
{ "program_run_id" }
|
||||
```
|
||||
```
|
||||
// Останавливает исполнение программы
|
||||
POST /v1/runs/{id}/cancel
|
||||
```
|
||||
##### Управление рантаймами
|
||||
```
|
||||
// Создаёт новый рантайм
|
||||
POST /v1/runtimes
|
||||
{ "coffee_machine_id", "program_id", "parameters" }
|
||||
→
|
||||
{ "runtime_id", "state" }
|
||||
```
|
||||
```
|
||||
// Возвращает текущее состояние рантайма
|
||||
// по его id
|
||||
GET /v1/runtimes/{runtime_id}/state
|
||||
{
|
||||
"status": "ready_waiting",
|
||||
// Текущая исполняемая команда (необязательное)
|
||||
"command_sequence_id",
|
||||
"resolution": "success",
|
||||
"variables"
|
||||
}
|
||||
```
|
||||
```
|
||||
// Прекращает исполнение рантайма
|
||||
POST /v1/runtimes/{id}/terminate
|
||||
```
|
Loading…
x
Reference in New Issue
Block a user