Перейти к основному содержимому

Устранение избыточности в RAML с помощью типов ресурсов и признаков

· 11 мин. чтения

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 } ]