Рубрики
Без рубрики

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

В этом руководстве мы показали, как значительно уменьшить или, в некоторых случаях, устранить избыточность в определении API RAML.

Автор оригинала: baeldung.

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