diff --git a/docs/API.en.epub b/docs/API.en.epub index c8dbcaa..d596362 100644 Binary files a/docs/API.en.epub and b/docs/API.en.epub differ diff --git a/docs/API.en.html b/docs/API.en.html index 1ee54f7..ea3aa20 100644 --- a/docs/API.en.html +++ b/docs/API.en.html @@ -240,6 +240,16 @@ a.anchor {  · https://www.linkedin.com/in/twirl/

+

+ The API-first development is one of the hottest technical topics in + 2020, since many companies started to realize that API serves as a + multiplicator to their opportunities—but it also amplifies the design + mistakes as well. +

+

+ The book is dedicated to designing APIs: how to build the architecture + properly, from a high-level planning down to final interfaces. +

Illustrations by Maria Konstantinova

@@ -265,7 +275,7 @@ a.anchor { -

Introduction

Chapter 1. On the Structure of This Book

+

Introduction

Chapter 1. On the Structure of This Book

The book you're holding in your hands comprises this Introduction and three large sections.

In Section I we'll discuss designing APIs as a concept: how to build the architecture properly, from a high-level planning down to final interfaces.

Section II is dedicated to an API's lifecycle: how interfaces evolve over time, and how to elaborate the product to match users' needs.

@@ -304,7 +314,7 @@ a.anchor {

Large companies, which occupy firm market positions, could afford implying such a taxation. Furthermore, they may introduce penalties for those who refuse to adapt their code to new API versions, up to disabling their applications.

From our point of view such practice cannot be justified. Don't imply hidden taxes on your customers. If you're able to avoid breaking backwards compatibility — never break it.

Of course, maintaining old API versions is a sort of a tax either. Technology changes, and you cannot foresee everything, regardless of how nice your API is initially designed. At some point keeping old API versions results in an inability to provide new functionality and support new platforms, and you will be forced to release new version. But at least you will be able to explain to your customers why they need to make an effort.

-

We will discuss API lifecycle and version policies in Section II.

Chapter 5. On versioning

+

We will discuss API lifecycle and version policies in Section II.

Chapter 5. On Versioning

Here and throughout we firmly stick to semver principles of versioning:

  1. API versions are denoted with three numbers, i.e. 1.2.3.
  2. @@ -647,7 +657,7 @@ GET /sensors { "order_id" }

    The POST /orders handler checks all order parameters, puts a hold of corresponding sum on user's credit card, forms a request to run, and calls the execution level. First, correct execution program needs to be fetched:

    -
    POST /v1/programs/match
    +
    POST /v1/program-matcher
     { "recipe", "coffee-machine" }
     →
     { "program_id" }
    @@ -669,7 +679,7 @@ GET /sensors
     

    Please note that knowing the coffee machine API kind isn't required at all; that's why we're making abstractions! We could possibly make interfaces more specific, implementing different run and match endpoints for different coffee machines:

      -
    • POST /v1/programs/{api_type}/match
    • +
    • POST /v1/program-matcher/{api_type}
    • POST /v1/programs/{api_type}/{program_id}/run

    This approach has some benefits, like a possibility to provide different sets of parameters, specific to the API kind. But we see no need in such fragmentation. run method handler is capable of extracting all the program metadata and perform one of two actions:

    @@ -885,7 +895,7 @@ let recipes = api.getRecipes(); // Retrieve a list of all available coffee machines let coffeeMachines = api.getCoffeeMachines(); // Build a spatial index -let coffeeMachineRecipesIndex = buildGeoIndex(recipes, coffee-machines); +let coffeeMachineRecipesIndex = buildGeoIndex(recipes, coffeeMachines); // Select coffee machines matching user's needs let matchingCoffeeMachines = coffeeMachineRecipesIndex.query( parameters, @@ -901,7 +911,7 @@ app.display(coffeeMachines);
  3. display nearby cafes where a user could order a particular type of coffee — for users seeking a certain beverage type.
  4. Then our new interface would look like:

    -
    POST /v1/coffee-machines/search
    +
    POST /v1/offers/search
     {
       // optional
       "recipes": ["lungo", "americano"],
    @@ -924,16 +934,16 @@ app.display(coffeeMachines);
     
  5. an offer — is a marketing bid: on what conditions a user could have the requested coffee beverage (if specified in request), or a some kind of marketing offering — prices for the most popular or interesting products (if no specific preference was set);
  6. a place — is a spot (café, restaurant, street vending machine) where the coffee machine is located; we never introduced this entity before, but it's quite obvious that users need more convenient guidance to find a proper coffee machine than just geographical coordinates.
  7. -

    NB. We could have been enriched the existing /coffee-machines endpoint instead of adding a new one. This decision, however, looks less semantically viable: coupling in one interface different modes of listing entities, by relevance and by order, is usually a bad idea, because these two types of rankings implies different usage features and scenarios.

    +

    NB. We could have been enriched the existing /coffee-machines endpoint instead of adding a new one. This decision, however, looks less semantically viable: coupling in one interface different modes of listing entities, by relevance and by order, is usually a bad idea, because these two types of rankings implies different usage features and scenarios. Furthermore, enriching the search with ‘offers’ pulls this functionality out of coffee-machines namespace: the fact of getting offers to prepare specific beverage in specific conditions is a key feature to users, with specifying the coffee-machine being just a part of an offer.

    Coming back to the code developers are writing, it would now look like that:

    -
    // Searching for coffee machines
    +
    // Searching for offers
     // matching a user's intent
    -let coffeeMachines = api.search(parameters);
    +let offers = api.search(parameters);
     // Display them to a user
    -app.display(coffeeMachines);
    +app.display(offers);
     

    Helpers

    -

    Methods similar to newly invented coffee-machines/search are called helpers. The purpose they exist is to generalize known API usage scenarios and facilitate implementing them. By ‘facilitating’ we mean not only reducing wordiness (getting rid of ‘boilerplates’), but also helping developers to avoid common problems and mistakes.

    +

    Methods similar to newly invented offers/search are called helpers. The purpose they exist is to generalize known API usage scenarios and facilitate implementing them. By ‘facilitating’ we mean not only reducing wordiness (getting rid of ‘boilerplates’), but also helping developers to avoid common problems and mistakes.

    For instance, let's consider the order price question. Our search function returns some ‘offers’ with prices. But ‘price’ is volatile; coffee could cost less during ‘happy hours’, for example. Developers could make a mistake thrice while implementing this functionality:

    • cache search results on a client device for too long (as a result, the price will always be nonactual);
    • @@ -1055,7 +1065,7 @@ The invalid price error is resolvable: client could obtain a new price offer and

    Let's try to group it together:

    {
    -  "results": {
    +  "results": [{
         // Place data
         "place": { "name", "location" },
         // Coffee machine properties
    @@ -1073,7 +1083,7 @@ The invalid price error is resolvable: client could obtain a new price offer and
           "pricing": { "currency_code", "price", "localized_price" },
           "estimated_waiting_time"
         }
    -  }
    +  }, …]
     }
     

    Such decomposed API is much easier to read than a long sheet of different attributes. Furthermore, it's probably better to group even more entities in advance. For example, place and route could be joined in a single location structure, or offer and pricing might be combined into a some generalized object.

    @@ -1895,6 +1905,147 @@ POST /v1/orders

    Sometimes explicit location passing is not enough since there are lots of territorial conflicts in a world. How the API should behave when user coordinates lie within disputed regions is a legal matter, regretfully. Author of this books once had to implement a ‘state A territory according to state B official position’ concept.

    Important: mark a difference between localization for end users and localization for developers. Take a look at the example in #12 rule: localized_message is meant for the user; the app should show it if there is no specific handler for this error exists in code. This message must be written in user's language and formatted according to user's location. But details.checks_failed[].message is meant to be read by developers examining the problem. So it must be written and formatted in a manner which suites developers best. In a software development world it usually means ‘in English’.

    Worth mentioning is that localized_ prefix in the example is used to differentiate messages to users from messages to developers. A concept like that must be, of course, explicitly stated in your API docs.

    -

    And one more thing: all strings must be UTF-8, no exclusions.

    +

    And one more thing: all strings must be UTF-8, no exclusions.

    Chapter 12. Annex to Section I. Generic API Example

    +

    Let's summarize the current state of our API study.

    +
    1. Offer search
    +
    POST /v1/offers/search
    +{
    +  // optional
    +  "recipes": ["lungo", "americano"],
    +  "position": <geographical coordinates>,
    +  "sort_by": [
    +    { "field": "distance" }
    +  ],
    +  "limit": 10
    +}
    +→
    +{
    +  "results": [{
    +    // Place data
    +    "place": { "name", "location" },
    +    // Coffee machine properties
    +    "coffee-machine": { "brand", "type" },
    +    // Route data
    +    "route": { "distance", "duration", "location_tip" },
    +    "offers": {
    +      // Recipe data
    +      "recipe": { "id", "name", "description" },
    +      // Recipe specific options
    +      "options": { "volume" },
    +      // Offer metadata
    +      "offer": { "id", "valid_until" },
    +      // Pricing
    +      "pricing": { "currency_code", "price", "localized_price" },
    +      "estimated_waiting_time"
    +    }
    +  }, …],
    +  "cursor"
    +}
    +
    +
    2. Working with recipes
    +
    // Returns a list of recipes
    +// Cursor parameter is optional
    +GET /v1/recipes?cursor=<cursor>
    +→
    +{ "recipes", "cursor" }
    +
    +
    // Returns the recipe by its id
    +GET /v1/recipes/{id}
    +→
    +{ "recipe_id", "name", "description" }
    +
    +
    3. Working with orders
    +
    // Creates an order
    +POST /v1/orders
    +{
    +  "coffee_machine_id",
    +  "currency_code",
    +  "price",
    +  "recipe": "lungo",
    +  // Optional
    +  "offer_id",
    +  // Optional
    +  "volume": "800ml"
    +}
    +→
    +{ "order_id" }
    +
    +
    // Returns the order by its id
    +GET /v1/orders/{id}
    +→
    +{ "order_id", "status" }
    +
    +
    // Cancels the order
    +POST /v1/orders/{id}/cancel
    +
    +
    4. Working with programs
    +
    // Returns an identifier of the program
    +// corresponding to specific recipe
    +// on specific coffee-machine
    +POST /v1/program-matcher
    +{ "recipe", "coffee-machine" }
    +→
    +{ "program_id" }
    +
    +
    // Return program description
    +// by its id
    +GET /v1/programs/{id}
    +→
    +{
    +  "program_id",
    +  "api_type",
    +  "commands": [
    +    {
    +      "sequence_id",
    +      "type": "set_cup",
    +      "parameters"
    +    },
    +    …
    +  ]
    +}
    +
    +
    5. Running programs
    +
    // Runs the specified program
    +// on the specefied coffee-machine
    +// with specific parameters
    +POST /v1/programs/{id}/run
    +{
    +  "order_id",
    +  "coffee_machine_id",
    +  "parameters": [
    +    {
    +      "name": "volume",
    +      "value": "800ml"
    +    }
    +  ]
    +}
    +→
    +{ "program_run_id" }
    +
    +
    // Stops program running
    +POST /v1/runs/{id}/cancel
    +
    +
    6. Managing runtimes
    +
    // Creates a new runtime
    +POST /v1/runtimes
    +{ "coffee_machine_id", "program_id", "parameters" }
    +→
    +{ "runtime_id", "state" }
    +
    +
    // Returns the state
    +// of the specified runtime
    +GET /v1/runtimes/{runtime_id}/state
    +{
    +  "status": "ready_waiting",
    +  // Command being currently executed
    +  // (optional)
    +  "command_sequence_id",
    +  "resolution": "success",
    +  "variables"
    +}
    +
    +
    // Terminates the runtime
    +POST /v1/runtimes/{id}/terminate
    +
    \ No newline at end of file diff --git a/docs/API.en.pdf b/docs/API.en.pdf index cf4e7c3..5da6905 100644 Binary files a/docs/API.en.pdf and b/docs/API.en.pdf differ diff --git a/docs/API.ru.epub b/docs/API.ru.epub index 3ed3e82..782c36c 100644 Binary files a/docs/API.ru.epub and b/docs/API.ru.epub differ diff --git a/docs/API.ru.html b/docs/API.ru.html index 17ad86a..c32f6ec 100644 --- a/docs/API.ru.html +++ b/docs/API.ru.html @@ -240,6 +240,17 @@ a.anchor {  · https://www.linkedin.com/in/twirl/

    +

    + «API-first» подход — одна из самых горячих горячих тем в разработке + программного обеспечения в 2020. Многие компании начали понимать, что + API выступает мультипликатором их возможностей — но также умножает и + допущенные ошибки. +

    +

    + Эта книга посвящена проектированию API: как правильно выстроить + архитектуру, начиная с высокоуровневого планирования из заканчивая + деталями реализации конкретных интерфейсов. +

    Иллюстрации: Мария Константинова

    @@ -264,7 +275,7 @@ a.anchor { -

    Введение

    Глава 1. О структуре этой книги

    +

    Введение

    Глава 1. О структуре этой книги

    Книга, которую вы держите в руках, состоит из введения и трех больших разделов.

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

    Второй раздел будет посвящён жизненному циклу API — как интерфейсы эволюционируют со временем и как развивать продукт так, чтобы отвечать потребностям пользователей.

    @@ -639,7 +650,7 @@ GET /sensors { "order_id" }

    Имплементация функции POST /orders проверит все параметры заказа, заблокирует его стоимость на карте пользователя, сформирует полный запрос на исполнение и обратится к уровню исполнения. Сначала необходимо подобрать правильную программу исполнения:

    -
    POST /v1/programs/match
    +
    POST /v1/program-matcher
     { "recipe", "coffee-machine" }
     →
     { "program_id" }
    @@ -661,7 +672,7 @@ GET /sensors
     

    Обратите внимание, что во всей этой цепочке вообще никак не участвует тип 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 сам может извлечь нужные параметры из мета-информации о программе и выполнить одно из двух действий:

    @@ -873,7 +884,7 @@ GET /sensors

    Очевидно, первый шаг — нужно предоставить пользователю возможность выбора, чего он, собственно хочет. И первый же шаг обнажает неудобство использования нашего API: никаких методов, позволяющих пользователю что-то выбрать в нашем API нет. Разработчику придётся сделать что-то типа такого:

    • получить все доступные рецепты из GET /v1/recipes;
    • -
    • получить список всех кофе-машины из GET /v1/coffee-machines;
    • +
    • получить список всех кофе-машин из GET /v1/coffee-machines;
    • самостоятельно выбрать нужные данные.

    В псевдокоде это будет выглядеть примерно вот так:

    @@ -882,7 +893,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, @@ -898,7 +909,7 @@ app.display(coffeeMachines);
  8. показать ближайшие кофейни, где можно заказать конкретный вид кофе — для пользователей, которым нужен конкретный напиток.
  9. Тогда наш новый интерфейс будет выглядеть примерно вот так:

    -
    POST /v1/coffee-machines/search
    +
    POST /v1/offers/search
     {
       // опционально
       "recipes": ["lungo", "americano"],
    @@ -921,15 +932,16 @@ app.display(coffeeMachines);
     
  10. offer — некоторое «предложение»: на каких условиях можно заказать запрошенные виды кофе, если они были указаны, либо какое-то маркетинговое предложение — цены на самые популярные / интересные напитки, если пользователь не указал конкретные рецепты для поиска;
  11. place — место (кафе, автомат, ресторан), где находится машина; мы не вводили эту сущность ранее, но, очевидно, пользователю потребуются какие-то более понятные ориентиры, нежели географические координаты, чтобы найти нужную кофе-машину.
  12. -

    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 и облегчить их. Под «облегчить» мы имеем в виду не только сократить многословность («бойлерплейт»), но и помочь разработчику избежать частых проблем и ошибок.

    Рассмотрим, например, вопрос стоимости заказа. Наша функция поиска возвращает какие-то «предложения» с ценой. Но ведь цена может меняться: в «счастливый час» кофе может стоить меньше. Разработчик может ошибиться в имплементации этой функциональности трижды:

    • кэшировать на клиентском устройстве результаты поиска слишком долго (в результате цена всегда будет неактуальна),
    • @@ -956,7 +968,7 @@ app.display(coffeeMachines); }

    Поступая так, мы не только помогаем разработчику понять, когда ему надо обновить цены, но и решаем UX-задачу: как показать пользователю, что «счастливый час» скоро закончится. Идентификатор предложения может при этом быть stateful (фактически, аналогом сессии пользователя) или stateless (если мы точно знаем, до какого времени действительна цены, мы может просто закодировать это время в идентификаторе).

    -

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

    +

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

    Обработка ошибок

    Сделаем ещё один небольшой шаг в сторону улучшения жизни разработчика. А каким образом будет выглядеть ошибка «неверная цена»?

    POST /v1/orders
    @@ -1058,7 +1070,7 @@ app.display(coffeeMachines);
     
     

    Попробуем сгруппировать:

    {
    -  "results": {
    +  "results": [{
         // Данные о заведении
         "place": { "name", "location" },
         // Данные о кофе-машине
    @@ -1078,7 +1090,7 @@ app.display(coffeeMachines);
           "pricing": { "currency_code", "price", "localized_price" },
           "estimated_waiting_time"
         }
    -  }
    +  }, …]
     }
     

    Такое API читать и воспринимать гораздо удобнее, нежели сплошную простыню различных атрибутов. Более того, возможно, стоит на будущее сразу дополнительно сгруппировать, например, place и route в одну структуру location, или offer и pricing в одну более общую структуру.

    @@ -1592,7 +1604,7 @@ GET /v1/recipes }] }
    -

    По сути, для клиента всё произошло ожидаемым образом: изменения были внесены, и последний полученный ответ всегда корректен. Однако по сути состояние ресурса после первого запросе отличалось от состояния ресурса после второго запроса, что противоречит самому определению идемпотентности.

    +

    По сути, для клиента всё произошло ожидаемым образом: изменения были внесены, и последний полученный ответ всегда корректен. Однако по сути состояние ресурса после первого запроса отличалось от состояния ресурса после второго запроса, что противоречит самому определению идемпотентности.

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

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

    15. Указывайте политики кэширования
    @@ -1900,6 +1912,150 @@ POST /v1/orders

    Следует иметь в виду, что явной передачи локации может оказаться недостаточно, поскольку в мире существуют территориальные конфликты и спорные территории. Каким образом API должно себя вести при попадании координат пользователя на такие территории — вопрос, к сожалению, в первую очередь юридический. Автору этой книги приходилось как-то разрабатывать API, в котором пришлось вводить концепцию «территория государства A по мнению официальных органов государства Б».

    Важно: различайте локализацию для конечного пользователя и локализацию для разработчика. В примере из п. 12 сообщение localized_message адресовано пользователю — его должно показать приложение, если в коде обработка такой ошибки не предусмотрена. Это сообщение должно быть написано на указанном в запросе языке и отформатировано согласно правилам локации пользователя. А вот сообщение details.checks_failed[].message написано не для пользователя, а для разработчика, который будет разбираться с проблемой. Соответственно, написано и отформатировано оно должно быть понятным для разработчика образом — что, скорее всего, означает «на английском языке», т.к. английский де факто является стандартом в мире разработки программного обеспечения.

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

    -

    И ещё одна вещь: все строки должны быть в кодировке UTF-8 и никакой другой.

    +

    И ещё одна вещь: все строки должны быть в кодировке UTF-8 и никакой другой.

    Глава 12. Приложение к разделу I. Модельное API

    +

    Суммируем текущее состояние нашего учебного API.

    +
    1. Поиск предложений
    +
    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"
    +}
    +
    +
    2. Работа с рецептами
    +
    // Возвращает список рецептов
    +// Параметр cursor необязателен
    +GET /v1/recipes?cursor=<курсор>
    +→
    +{ "recipes", "cursor" }
    +
    +
    // Возвращает конкретный рецепт
    +// по его идентификатору
    +GET /v1/recipes/{id}
    +→
    +{ "recipe_id", "name", "description" }
    +
    +
    3. Работа с заказами
    +
    // Размещает заказ
    +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
    +
    +
    4. Работа с программами
    +
    // Возвращает идентификатор программы,
    +// соответствующей указанному рецепту
    +// на указанной кофе-машине
    +POST /v1/program-matcher
    +{ "recipe", "coffee-machine" }
    +→
    +{ "program_id" }
    +
    +
    // Возвращает описание
    +// программы по её идентификатору
    +GET /v1/programs/{id}
    +→
    +{
    +  "program_id",
    +  "api_type",
    +  "commands": [
    +    {
    +      "sequence_id",
    +      "type": "set_cup",
    +      "parameters"
    +    },
    +    …
    +  ]
    +}
    +
    +
    5. Исполнение программ
    +
    // Запускает исполнение программы
    +// с указанным идентификатором
    +// на указанной машине
    +// с указанными параметрами
    +POST /v1/programs/{id}/run
    +{
    +  "order_id",
    +  "coffee_machine_id",
    +  "parameters": [
    +    {
    +      "name": "volume",
    +      "value": "800ml"
    +    }
    +  ]
    +}
    +→
    +{ "program_run_id" }
    +
    +
    // Останавливает исполнение программы
    +POST /v1/runs/{id}/cancel
    +
    +
    6. Управление рантаймами
    +
    // Создаёт новый рантайм
    +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
    +
    \ No newline at end of file diff --git a/docs/API.ru.pdf b/docs/API.ru.pdf index 9e8efa0..1d6d93b 100644 Binary files a/docs/API.ru.pdf and b/docs/API.ru.pdf differ