1. Обзор
В нашей учебной статье по RAML мы представили язык моделирования RESTful API
и создали простое определение API, основанное на единственном объекте с именем Foo
. Теперь представьте реальный API, в котором у вас есть несколько ресурсов сущностного типа с одинаковыми или похожими операциями GET, POST, PUT и DELETE. Вы видите, как ваша документация по API может быстро стать утомительной и повторяющейся.
В этой статье мы покажем, как использование функций типов
и свойств
ресурсов в RAML может устранить избыточность в определениях ресурсов и методов путем извлечения и параметризации общих разделов, тем самым устраняя ошибки копирования и вставки и делая определения API более краткими.
2. Наш API
Чтобы продемонстрировать преимущества типов
и характеристик
ресурсов , мы расширим наш исходный API, добавив ресурсы для второго типа сущности, называемого Bar
. Вот ресурсы, которые составят наш пересмотренный API:
ПОЛУЧИТЬ /api/v1/foos
ПОСТ /api/v1/foos
ПОЛУЧИТЬ /api/v1/foos/{fooId}
ПОСТАВЬТЕ /api/v1/foos/{fooId}
УДАЛИТЬ /api/v1/foos/{fooId}
ПОЛУЧИТЬ /api/v1/foos/имя/{имя}
ПОЛУЧИТЬ /api/v1/foos?name={name}&ownerName={ownerName}
ПОЛУЧИТЬ /api/v1/бары
ПОСТ/апи/v1/бары
ПОЛУЧИТЬ /api/v1/bars/{barId}
ПОСТАВЬТЕ /api/v1/bars/{barId}
УДАЛИТЬ /api/v1/bars/{barId}
ПОЛУЧИТЬ /api/v1/bars/fooId/{fooId}
3. Распознавание шаблонов
Когда мы читаем список ресурсов в нашем API, мы начинаем видеть некоторые закономерности. Например, существует шаблон для URI и методов, используемых для создания, чтения, обновления и удаления отдельных сущностей, а также шаблон для URI и методов, используемых для извлечения коллекций сущностей. Шаблон коллекции и элемента коллекции является одним из наиболее распространенных шаблонов, используемых для извлечения типов ресурсов
в определениях RAML.
Давайте посмотрим на пару разделов нашего API:
[Примечание: в приведенных ниже фрагментах кода строка, содержащая только три точки (…), указывает на то, что некоторые строки для краткости пропущены.]
/foos:
get:
description: |
List all foos matching query criteria, if provided;
otherwise list all foos
queryParameters:
name?: string
ownerName?: string
responses:
200:
body:
application/json:
type: Foo[]
post:
description: Create a new foo
body:
application/json:
type: Foo
responses:
201:
body:
application/json:
type: Foo
...
/bars:
get:
description: |
List all bars matching query criteria, if provided;
otherwise list all bars
queryParameters:
name?: string
ownerName?: string
responses:
200:
body:
application/json:
type: Bar[]
post:
description: Create a new bar
body:
application/json:
type: Bar
responses:
201:
body:
application/json:
type: Bar
Когда мы сравниваем RAML-определения ресурсов /foos
и /bars
, включая используемые HTTP-методы, мы можем увидеть несколько дублирующих друг друга свойств каждого из них, и мы снова видим, как начинают появляться закономерности.
Везде, где есть шаблон в определении ресурса или метода, есть возможность использовать тип
или признак
ресурса RAML .
4. Типы ресурсов
Чтобы реализовать шаблоны, найденные в API, типы ресурсов
используют зарезервированные и определяемые пользователем параметры, заключенные в двойные угловые скобки (<< и >>).
4.1. Зарезервированные параметры
В определениях типов ресурсов могут использоваться два зарезервированных параметра:
<<resourcePath>>
представляет собой весь URI (послеbaseURI
), и<<resourcePathName>>
представляет часть URI после крайней правой косой черты (/), игнорируя фигурные скобки { }.
При обработке внутри определения ресурса их значения рассчитываются на основе определяемого ресурса.
Учитывая ресурс /foos
, например, <<resourcePath>>
оценивается как «/foos», а <<resourcePathName>>
оценивается как «foos».
Учитывая ресурс /foos/{fooId}
, <<resourcePath>>
оценивается как «/foos/{fooId}», а <<resourcePathName>>
оценивается как «foos».
4.2. Пользовательские параметры
Определение типа ресурса
может также содержать определяемые пользователем параметры. В отличие от зарезервированных параметров, значения которых определяются динамически на основе определяемого ресурса, определяемым пользователем параметрам должны присваиваться значения везде, где используется тип ресурса,
содержащий их, и эти значения не изменяются.
Пользовательские параметры могут быть объявлены в начале определения типа ресурса
, хотя это не требуется и не является общепринятой практикой, поскольку читатель обычно может определить их предполагаемое использование, зная их имена и контексты, в которых они используются.
4.3. Функции параметров
Несколько полезных текстовых функций доступны для использования везде, где используется параметр, для преобразования расширенного значения параметра при его обработке в определении ресурса.
Вот функции, доступные для преобразования параметров:
- !
выделять в единственном числе
- !
множественное число
- !
верхний регистр
- !
нижний регистр
- !
верхний верблюжий регистр
- !
нижний верблюжий регистр
- !
верхний подчеркивание
- !
нижнее подчеркивание
- !
верхний дефис
- !
нижний дефис
Функции применяются к параметру с помощью следующей конструкции:
<< имя_параметра
| ! имя_функции
>>
Если вам нужно использовать более одной функции для достижения желаемого преобразования, вы должны отделить каждое имя функции символом вертикальной черты («|») и добавить восклицательный знак (!) перед каждой используемой функцией.
Например, для ресурса /foos
, где << resourcePathName
>> оценивается как «foos»:
- <<
имя_ресурса
| !единственное число
>> ==> «foo» - <<
имя_ресурса
| !верхний регистр
>> ==> «FOOS» - <<
имя_ресурса
| !унифицировать
| !верхний регистр
>> ==> «FOO»
И учитывая ресурс /bars/{barId}
, где << resourcePathName
>> оценивается как «bars»:
- <<
имя_ресурса
| !верхний регистр
>> ==> «БАРС» - <<
имя_ресурса
| !верхний верблюжий регистр
>> ==> «Бар»
5. Извлечение типа ресурса для коллекций
Давайте реорганизуем определения ресурсов /foos
и /bars
, показанные выше, используя тип ресурса
для захвата общих свойств. Мы будем использовать зарезервированный параметр <<resourcePathName>>
и определяемый пользователем параметр <<typeName>>
для представления используемого типа данных.
5.1. Определение
Вот определение типа ресурса,
представляющее набор элементов:
resourceTypes:
collection:
usage: Use this resourceType to represent any collection of items
description: A collection of <<resourcePathName>>
get:
description: Get all <<resourcePathName>>, optionally filtered
responses:
200:
body:
application/json:
type: <<typeName>>[]
post:
description: Create a new <<resourcePathName|!singularize>>
responses:
201:
body:
application/json:
type: <<typeName>>
Обратите внимание, что в нашем API, поскольку наши типы данных представляют собой просто заглавные версии имен наших базовых ресурсов в единственном числе, мы могли бы применить функции к параметру << resourcePathName
>> вместо введения определяемого пользователем параметра << typeName
>> , чтобы добиться того же результата для этой части API:
resourceTypes:
collection:
...
get:
...
type: <<resourcePathName|!singularize|!uppercamelcase>>[]
post:
...
type: <<resourcePathName|!singularize|!uppercamelcase>>
5.2. Заявление
Используя приведенное выше определение, включающее параметр << typeName
>>, вот как можно применить тип ресурса «коллекция» к
ресурсам /foos
и / bars
:
/foos:
type: { collection: { "typeName": "Foo" } }
get:
queryParameters:
name?: string
ownerName?: string
...
/bars:
type: { collection: { "typeName": "Bar" } }
Обратите внимание, что мы по-прежнему можем учитывать различия между двумя ресурсами — в данном случае разделом queryParameters
— и в то же время пользоваться всеми преимуществами, которые может предложить определение типа ресурса .
6. Извлечение типа ресурса для отдельных элементов коллекции
Теперь давайте сосредоточимся на части нашего API, связанной с отдельными элементами коллекции: ресурсах /foos/{fooId}
и /bars/{barId}
. Вот код для /foos/{fooId}
:
/foos:
...
/{fooId}:
get:
description: Get a Foo
responses:
200:
body:
application/json:
type: Foo
404:
body:
application/json:
type: Error
example: !include examples/Error.json
put:
description: Update a Foo
body:
application/json:
type: Foo
responses:
200:
body:
application/json:
type: Foo
404:
body:
application/json:
type: Error
example: !include examples/Error.json
delete:
description: Delete a Foo
responses:
204:
404:
body:
application/json:
type: Error
example: !include examples/Error.json
Определение ресурса /bars/{barId}
также имеет методы GET, PUT и DELETE и идентично определению / foos/{fooId}
, за исключением вхождений строк «foo» и «bar» (и их соответствующих множественных значений ). и/или заглавные формы).
6.1. Определение
Извлекая шаблон, который мы только что определили, вот как мы определяем тип ресурса
для отдельных элементов коллекции:
resourceTypes:
...
item:
usage: Use this resourceType to represent any single item
description: A single <<typeName>>
get:
description: Get a <<typeName>>
responses:
200:
body:
application/json:
type: <<typeName>>
404:
body:
application/json:
type: Error
example: !include examples/Error.json
put:
description: Update a <<typeName>>
body:
application/json:
type: <<typeName>>
responses:
200:
body:
application/json:
type: <<typeName>>
404:
body:
application/json:
type: Error
example: !include examples/Error.json
delete:
description: Delete a <<typeName>>
responses:
204:
404:
body:
application/json:
type: Error
example: !include examples/Error.json
6.2. Заявление
А вот как мы применяем тип ресурса
«item» :
/foos:
...
/{fooId}:
type: { item: { "typeName": "Foo" } }
...
/bars:
...
/{barId}:
type: { item: { "typeName": "Bar" } }
7. Черты
В то время как тип ресурса
используется для извлечения шаблонов из определений ресурсов, трейт
используется для извлечения шаблонов из определений методов, общих для ресурсов.
7.1. Параметры
Наряду с << resourcePath
>> и << resourcePathName
>> для использования в определениях трейтов доступен один дополнительный зарезервированный параметр: << methodName
>> оценивает метод HTTP (GET, POST, PUT, DELETE и т. д.), для которого черта
определена. Определяемые пользователем параметры также могут появляться в определении признаков и, если они применяются, принимают значение ресурса, в котором они применяются.
7.2. Определение
Обратите внимание, что тип ресурса
«предмет» по- прежнему полон избыточности. Давайте посмотрим, как черты
могут помочь устранить их. Мы начнем с извлечения типажа
для любого метода, содержащего тело запроса:
traits:
hasRequestItem:
body:
application/json:
type: <<typeName>>
Теперь давайте извлечем трейты
для методов, обычные ответы которых содержат тела:
hasResponseItem:
responses:
200:
body:
application/json:
type: <<typeName>>
hasResponseCollection:
responses:
200:
body:
application/json:
type: <<typeName>>[]
Наконец, вот трейт
для любого метода, который может вернуть ответ об ошибке 404:
hasNotFound:
responses:
404:
body:
application/json:
type: Error
example: !include examples/Error.json
7.3. Заявление
Затем мы применяем этот трейт
к нашим типам ресурсов
:
resourceTypes:
collection:
usage: Use this resourceType to represent any collection of items
description: A collection of <<resourcePathName|!uppercamelcase>>
get:
description: |
Get all <<resourcePathName|!uppercamelcase>>,
optionally filtered
is: [ hasResponseCollection: { typeName: <<typeName>> } ]
post:
description: Create a new <<resourcePathName|!singularize>>
is: [ hasRequestItem: { typeName: <<typeName>> } ]
item:
usage: Use this resourceType to represent any single item
description: A single <<typeName>>
get:
description: Get a <<typeName>>
is: [ hasResponseItem: { typeName: <<typeName>> }, hasNotFound ]
put:
description: Update a <<typeName>>
is: | [ hasRequestItem: { typeName: <<typeName>> }, hasResponseItem: { typeName: <<typeName>> }, hasNotFound ]
delete:
description: Delete a <<typeName>>
is: [ hasNotFound ]
responses:
204:
Мы также можем применять трейты
к методам, определенным в ресурсах. Это особенно полезно для «одноразовых» сценариев, когда комбинация ресурса и метода соответствует одному или нескольким признакам
, но не соответствует какому-либо определенному типу ресурса
:
/foos:
...
/name/{name}:
get:
description: List all foos with a certain name
is: [ hasResponseCollection: { typeName: Foo } ]
8. Заключение
В этом руководстве мы показали, как значительно уменьшить или, в некоторых случаях, устранить избыточность в определении RAML API.
Во-первых, мы определили избыточные разделы наших ресурсов, распознали их закономерности и извлекли типы ресурсов
. Затем мы сделали то же самое для общих для ресурсов методов извлечения признаков
. Затем мы смогли устранить дополнительную избыточность, применяя трейты
к нашим типам ресурсов
и к «одноразовым» комбинациям ресурсов и методов, которые строго не соответствовали одному из определенных нами типов ресурсов
.
В результате наш простой API с ресурсами только для двух сущностей сократился со 177 до чуть более 100 строк кода. Чтобы узнать больше о типах и свойствах
ресурсов
RAML , посетите спецификацию RAML.org 1.0 . ``
Полную реализацию этого туториала можно найти в проекте github .
Вот наш окончательный RAML API целиком:
#%RAML 1.0
title: ForEach Foo REST Services API
version: v1
protocols: [ HTTPS ]
baseUri: http://rest-api.foreach.com/api/{version}
mediaType: application/json
securedBy: basicAuth
securitySchemes:
basicAuth:
description: |
Each request must contain the headers necessary for
basic authentication
type: Basic Authentication
describedBy:
headers:
Authorization:
description: |
Used to send the Base64 encoded "username:password"
credentials
type: string
responses:
401:
description: |
Unauthorized. Either the provided username and password
combination is invalid, or the user is not allowed to
access the content provided by the requested URL.
types:
Foo: !include types/Foo.raml
Bar: !include types/Bar.raml
Error: !include types/Error.raml
resourceTypes:
collection:
usage: Use this resourceType to represent a collection of items
description: A collection of <<resourcePathName|!uppercamelcase>>
get:
description: |
Get all <<resourcePathName|!uppercamelcase>>,
optionally filtered
is: [ hasResponseCollection: { typeName: <<typeName>> } ]
post:
description: |
Create a new <<resourcePathName|!uppercamelcase|!singularize>>
is: [ hasRequestItem: { typeName: <<typeName>> } ]
item:
usage: Use this resourceType to represent any single item
description: A single <<typeName>>
get:
description: Get a <<typeName>>
is: [ hasResponseItem: { typeName: <<typeName>> }, hasNotFound ]
put:
description: Update a <<typeName>>
is: [ hasRequestItem: { typeName: <<typeName>> }, hasResponseItem: { typeName: <<typeName>> }, hasNotFound ]
delete:
description: Delete a <<typeName>>
is: [ hasNotFound ]
responses:
204:
traits:
hasRequestItem:
body:
application/json:
type: <<typeName>>
hasResponseItem:
responses:
200:
body:
application/json:
type: <<typeName>>
hasResponseCollection:
responses:
200:
body:
application/json:
type: <<typeName>>[]
hasNotFound:
responses:
404:
body:
application/json:
type: Error
example: !include examples/Error.json
/foos:
type: { collection: { typeName: Foo } }
get:
queryParameters:
name?: string
ownerName?: string
/{fooId}:
type: { item: { typeName: Foo } }
/name/{name}:
get:
description: List all foos with a certain name
is: [ hasResponseCollection: { typeName: Foo } ]
/bars:
type: { collection: { typeName: Bar } }
/{barId}:
type: { item: { typeName: Bar } }
/fooId/{fooId}:
get:
description: Get all bars for the matching fooId
is: [ hasResponseCollection: { typeName: Bar } ]