«1. Обзор
В нашей учебной статье по RAML мы представили язык моделирования 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/{fooId} PUT /api/v1/foos/{fooId } DELETE /api/v1/foos/{fooId} GET /api/v1/foos/name/{name} GET /api/v1/foos?name={name}\u0026ownerName={ownerName} GET /api/v1/bars POST /api/v1/bars GET /api/v1/bars/{barId} PUT /api/v1/bars/{barId} DELETE /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, типы ресурсов используют зарезервированные и определяемые пользователем параметры, заключенные в двойные угловые скобки (\u003c\u003c и \u003e\u003e).
4.1 Зарезервированные параметры
В определениях типов ресурсов могут использоваться два зарезервированных параметра:
-
\u003c\u003cresourcePath\u003e\u003e представляет собой полный URI (после baseURI), а \u003c\u003cresourcePathName\u003e\u003e представляет часть URI после крайней правой косой черты (/), игнорируя фигурные скобки { }.
При обработке внутри определения ресурса их значения рассчитываются на основе определяемого ресурса.
Учитывая ресурс /foos, например, \u003c\u003cresourcePath\u003e\u003e оценивается как «/foos», а \u003c\u003cresourcePathName\u003e\u003e оценивается как «foos».
Учитывая ресурс /foos/{fooId}, \u003c\u003cresourcePath\u003e\u003e оценивается как «/foos/{fooId}», а \u003c\u003cresourcePathName\u003e\u003e оценивается как «foos».
4.2 Пользовательские параметры
Определение типа ресурса может также содержать пользовательские параметры. В отличие от зарезервированных параметров, значения которых определяются динамически на основе определяемого ресурса, определяемым пользователем параметрам должны присваиваться значения везде, где используется тип ресурса, содержащий их, и эти значения не изменяются.
Пользовательские параметры могут быть объявлены в начале определения типа ресурса, хотя это не требуется и не является общепринятой практикой, поскольку читатель обычно может понять их предполагаемое использование, зная их имена и контексты, в которых они используются. используются.
4.3 Функции параметров
Несколько полезных текстовых функций доступны для использования везде, где используется параметр, для преобразования расширенного значения параметра при его обработке в определении ресурса.
«Вот функции, доступные для преобразования параметров:
-
!singularize !pluralize !uppercase !lowercase !uppercamelcase !lowercamelcase !upperunderscorecase !lowerunderscorecase !upperhyphencase !lowerhyphencase
Функции применяются к параметру с использованием следующей конструкции:
\u003c \u003cимя_параметра | !functionName\u003e\u003e
Если вам нужно использовать более одной функции для достижения желаемого преобразования, вы должны отделить каждое имя функции символом вертикальной черты (“|†) и добавить восклицательный знак (!) перед каждым используемая функция.
Например, для ресурса /foos, где \u003c\u003cresourcePathName\u003e\u003e оценивается как «foos»:
-
\u003c\u003cresourcePathName | !singularize\u003e\u003e ==\u003e «foo» \u003c\u003cresourcePathName | !uppercase\u003e\u003e ==\u003e «FOOS» \u003c\u003cresourcePathName | !единственное число | !uppercase\u003e\u003e ==\u003e \»FOO\»
И учитывая ресурс /bars/{barId}, где \u003c\u003cresourcePathName\u003e\u003e оценивается как \»bars\»:
-
\u003c\u003cresourcePathName | !uppercase\u003e\u003e ==\u003e «BARS» \u003c\u003cresourcePathName | !uppercamelcase\u003e\u003e ==\u003e «Bar»
5. Извлечение типа ресурса для коллекций
Давайте реорганизуем определения ресурсов /foos и /bars, показанные выше, используя тип ресурса для захвата общих свойств. Мы будем использовать зарезервированный параметр \u003c\u003cresourcePathName\u003e\u003e и определяемый пользователем параметр \u003c\u003ctypeName\u003e\u003e для представления используемого типа данных.
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, поскольку наши типы данных представляют собой просто заглавные версии имен наших базовых ресурсов в единственном числе, мы можно было применить функции к параметру \u003c\u003cresourcePathName\u003e\u003e вместо введения определяемого пользователем параметра \u003c\u003ctypeName\u003e\u003e для достижения того же результата для этой части API:
resourceTypes:
collection:
...
get:
...
type: <<resourcePathName|!singularize|!uppercamelcase>>[]
post:
...
type: <<resourcePathName|!singularize|!uppercamelcase>>
5.2 Приложение
Используя приведенное выше определение, включающее параметр \u003c\u003ctypeName\u003e\u003e, вот как можно применить тип ресурса «коллекция» к ресурсам /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 Параметры
Наряду с \u003c\u003cresourcePath\u003e\u003e и \u003c\u003cresourcePathName\u003e\u003e, один дополнительный зарезервированный параметр доступен для использования в определениях трейтов: \u003c\u003cmethodName\u003e\u003e возвращает метод HTTP (GET, POST, PUT , DELETE и т. д.), для которых определена черта. Определяемые пользователем параметры также могут появляться в определении признаков и, если они применяются, принимают значение ресурса, в котором они применяются.
7.2 Определение
traits:
hasRequestItem:
body:
application/json:
type: <<typeName>>
Обратите внимание, что тип ресурса «item» по-прежнему полон избыточности. Давайте посмотрим, как черты могут помочь устранить их. Мы начнем с извлечения типажа для любого метода, содержащего тело запроса:
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
Наконец, вот признак для любого метода, который может вернуть ответ об ошибке 404:
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.
#%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 <<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 } ]