1. Обзор
В нашей статье UML tutorial мы представили язык моделирования RESTful API и создали простое определение API, основанное на одной сущности с именем Foo . Теперь представьте себе реальный API, в котором у вас есть несколько ресурсов типа сущностей, все с одинаковыми или похожими операциями GET, POST, PUT и DELETE. Вы можете видеть, как ваша документация по API может быстро стать утомительной и повторяющейся.
В этой статье мы покажем, как использование типов ресурсов и признаков функций в RAML может устранить избыточность в определениях ресурсов и методов путем извлечения и параметризации общих разделов, тем самым устраняя ошибки копирования и вставки, делая ваши определения API более краткими.
2. Наш API
Чтобы продемонстрировать преимущества типов ресурсов и признаков , мы расширим наш исходный API, добавив ресурсы для второго типа сущностей с именем Bar . Вот ресурсы, которые составят наш пересмотренный API:
- GET/api/v1/foos
- POST/api/v1/foos
- GET/api/v1/foos/{foo Id}
- PUT/api/v1/foos/{fooId}
- УДАЛИТЬ/api/v1/foos/{fooId}
- GET/api/v1/foos/name/{имя}
- GET/api/v1/foos?name={name}&OwnerName={OwnerName}
- GET/api/v1/bars
- POST/api/v1/bars
- GET/api/v1/bars/{barId}
- PUT/api/v1/bars/{barId}
- УДАЛИТЬ/api/v1/bars/{barId}
- GET/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 Зарезервированные параметры
В определениях типов ресурсов могут использоваться два зарезервированных параметра:
- <<<<путь к ресурсу>> представляет весь URL-адрес (следующий за baseUri ), и
- <<<<путь к ресурсу>> представляет часть URI, следующую за самой правой косой чертой (/), игнорируя любые фигурные скобки { }.
При обработке внутри определения ресурса их значения вычисляются на основе определенного ресурса.
Учитывая ресурс /foods , например, <> будет оцениваться как “/food” и <<путь к ресурсу>> будет оцениваться как “food”.
Учитывая ресурс /foos/{fooId} , <> будет оцениваться как “/food/{food}” и <<путь к ресурсу>> будет оцениваться как “food”.
4.2 Определяемые пользователем параметры
Определение типа ресурса также может содержать пользовательские параметры. В отличие от зарезервированных параметров, значения которых определяются динамически на основе определяемого ресурса, определяемым пользователем параметрам должны присваиваться значения везде, где используется тип ресурса , содержащий их, и эти значения не изменяются.
Определяемые пользователем параметры могут быть объявлены в начале определения типа ресурса , хотя это не требуется и не является обычной практикой, поскольку читатель обычно может определить их предполагаемое использование, учитывая их имена и контексты, в которых они используются.
4.3 Функции параметров
Несколько полезных текстовых функций доступны для использования везде, где используется параметр, чтобы преобразовать расширенное значение параметра при его обработке в определении ресурса.
Вот функции, доступные для преобразования параметров:
- ! сингулярность
- ! плюрализация
- ! верхний регистр
- ! строчные буквы
- ! верхний регистр
- ! lowercamelcase
- ! !
- верхний регистр подчеркивания !
- ! нижний регистр подчеркивания
- ! !
Функции применяются к параметру с помощью следующей конструкции:
<< << Имя параметра | ! имя функции
Если вам нужно использовать более одной функции для достижения желаемого преобразования, вы должны отделить каждое имя функции символом канала (“|”) и добавить восклицательный знак (!) Перед каждой используемой функцией.
Например, учитывая ресурс /foos , где << resourcePathName >> принимает значение “false”:
- << resourcePathName | ! сингулярность >> ==> “foo”
- << << путь к ресурсу | ! верхний регистр
- >> ==> “FOOS” << resourcePathName | ! сингуляризация | ! верхний регистр
И учитывая ресурс /bars/{barId} , где << resourcePathName >> принимает значение “bars”:
- << << путь к ресурсу | ! верхний регистр
- >> ==> “БАРЫ” << << путь к ресурсу | !
5. Извлечение типа ресурса для коллекций
Давайте проведем рефакторинг /foos и /баров определений ресурсов, показанных выше, используя тип ресурса для захвата общих свойств. Мы будем использовать зарезервированный параметр <<путь к ресурсу>> и определяемый пользователем параметр <<Имя типа>> для представления используемого типа данных.
5.1 Определение
Вот определение типа ресурса , представляющее коллекцию элементов:
resourceTypes: collection: usage: Use this resourceType to represent any collection of items description: A collection of <> get: description: Get all < >, optionally filtered responses: 200: body: application/json: type: < >[] post: description: Create a new < > responses: 201: body: application/json: type: < >
Обратите внимание, что в нашем API, поскольку наши типы данных являются просто заглавными, сингулярными версиями имен наших базовых ресурсов, мы могли бы применить функции к параметру << resourcePathName >>, вместо того чтобы вводить определяемый пользователем параметр << typeName >>, чтобы достичь того же результата для этой части API:
resourceTypes: collection: ... get: ... type: <>[] post: ... type: < >
5.2 Применение
Используя приведенное выше определение, включающее параметр << typeName >>, вот как вы примените “коллекцию” тип ресурса к ресурсам /foos и/ барам :
/foos: type: { collection: { "typeName": "Foo" } } get: queryParameters: name?: string ownerName?: string ... /bars: type: { collection: { "typeName": "Bar" } }
Обратите внимание, что мы все еще можем включить различия между двумя ресурсами — в данном случае раздел queryParameters — и при этом использовать все преимущества, которые может предложить определение типа ресурса|/.
6. Извлечение типа ресурса для отдельных элементов коллекции
Давайте теперь сосредоточимся на той части нашего API, которая имеет дело с отдельными элементами коллекции: /foos/{fooId} и /bars/{barId} ресурсы. Вот код для /foos/{foo Id} :
/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 <> get: description: Get a < > responses: 200: body: application/json: type: < > 404: body: application/json: type: Error example: !include examples/Error.json put: description: Update a < > body: application/json: type: < > responses: 200: body: application/json: type: < > 404: body: application/json: type: Error example: !include examples/Error.json delete: description: Delete a < > responses: 204: 404: body: application/json: type: Error example: !include examples/Error.json
6.2 Применение
И вот как мы применяем “элемент” тип ресурса :
/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: <>
Теперь давайте извлекем черты для методов, нормальные ответы которых содержат тела:
hasResponseItem: responses: 200: body: application/json: type: <> hasResponseCollection: responses: 200: body: application/json: type: < >[]
Наконец, вот черта для любого метода, который может вернуть ответ на ошибку 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 <> get: description: | Get all < >, optionally filtered is: [ hasResponseCollection: { typeName: < > } ] post: description: Create a new < > is: [ hasRequestItem: { typeName: < > } ] item: usage: Use this resourceType to represent any single item description: A single < > get: description: Get a < > is: [ hasResponseItem: { typeName: < > }, hasNotFound ] put: description: Update a < > is: | [ hasRequestItem: { typeName: < > }, hasResponseItem: { typeName: < > }, hasNotFound ] delete: description: Delete a < > is: [ hasNotFound ] responses: 204:
Мы также можем применить черты к методам, определенным в ресурсах. Это особенно полезно для “одноразовых” сценариев, когда комбинация ресурсов и методов соответствует одному или нескольким признакам , но не соответствует определенному типу ресурсов :
/foos: ... /name/{name}: get: description: List all foos with a certain name is: [ hasResponseCollection: { typeName: Foo } ]
8. Заключение
В этом руководстве мы показали, как значительно уменьшить или, в некоторых случаях, устранить избыточность в определении API RAML.
Во-первых, мы определили избыточные разделы наших ресурсов, распознали их шаблоны и извлекли типы ресурсов . Затем мы сделали то же самое для методов, которые были общими для всех ресурсов, чтобы извлечь черты . Затем мы смогли устранить дальнейшие избыточности, применив черты к нашим типам ресурсов и к “одноразовым” комбинациям методов ресурсов, которые не строго соответствовали одному из наших определенных типов ресурсов .
В результате наш простой API с ресурсами только для двух сущностей был сокращен со 177 до чуть более 100 строк кода. Чтобы узнать больше о RAML типах ресурсов и чертах , посетите RAML.org 1.0 spec .
Полную реализацию этого руководства можно найти в проекте github .
Вот наш окончательный API RAML в полном объеме:
#%RAML 1.0 title: Baeldung Foo REST Services API version: v1 protocols: [ HTTPS ] baseUri: http://rest-api.baeldung.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 <> get: description: | Get all < >, optionally filtered is: [ hasResponseCollection: { typeName: < > } ] post: description: | Create a new < > is: [ hasRequestItem: { typeName: < > } ] item: usage: Use this resourceType to represent any single item description: A single < > get: description: Get a < > is: [ hasResponseItem: { typeName: < > }, hasNotFound ] put: description: Update a < > is: [ hasRequestItem: { typeName: < > }, hasResponseItem: { typeName: < > }, hasNotFound ] delete: description: Delete a < > is: [ hasNotFound ] responses: 204: traits: hasRequestItem: body: application/json: type: < > hasResponseItem: responses: 200: body: application/json: type: < > hasResponseCollection: responses: 200: body: application/json: type: < >[] 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 } ]