利用資源類型和特性的優勢,消除RAML中的冗餘

REST
Remote
0
04:49 PM · Dec 01 ,2025

1. 概述

在我們的RAML教程文章中,我們介紹了RESTful API建模語言並基於一個名為Foo的單實體創建了一個簡單的API定義。現在想象一個真實世界的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}&ownerName={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: |
      列出所有名稱匹配查詢條件的 foos,如果提供查詢條件;
      否則列出所有 foos
    queryParameters:
      name?: string
      ownerName?: string
    responses:
      200:
        body:
          application/json:
            type: Foo[]
  post:
    description: 創建一個新的 foo
    body:
      application/json:
        type: Foo
    responses:
      201:
        body:
          application/json:
            type: Foo
...
/bars:
  get:
    description: |
      列出所有名稱匹配查詢條件的 bars,如果提供查詢條件;
      否則列出所有 bars
    queryParameters:
      name?: string
      ownerName?: string
    responses:
      200:
        body:
          application/json:
            type: Bar[]
  post:
    description: 創建一個新的 bar
    body:
      application/json:
        type: Bar
    responses:
      201:
        body:
          application/json:
            type: Bar

當我們比較 RAML 定義的 /foos/bars 資源,包括使用的 HTTP 方法時,我們可以看到各個屬性中存在一些冗餘,並且我們再次看到模式開始出現。

在資源或方法定義中存在任何模式時,都有機會使用 RAML 的 資源類型特性

4.1. Reserved Parameters

為了實現API中發現的模式,resource types 使用預留和用户自定義的參數,這些參數被雙引號包圍 (<< 和 >>)。

4.1. Reserved Parameters

兩個預留參數可用於resource type定義:

  • <<resourcePath>> 代表整個URI(遵循baseURI),
  • <<resourcePathName>> 代表URI右側最右側斜槓(/)之後的部分,忽略花括號 { }。

4.2. User-Defined Parameters

一個resource type 定義也可能包含用户自定義的參數。與預留參數不同,其值是根據正在定義的資源動態計算的,而用户自定義參數的值必須在resource type 包含它們的地方賦值,並且這些值不會改變。

用户自定義參數可以在resource type 定義的開頭聲明,儘管這樣做並非必需,也不常見,因為讀者通常可以根據參數的名稱和它們被使用的上下文來推斷其預期用途。

4.3. Parameter Functions

有若干個有用的文本函數可用於在參數被用於resource definition中轉換參數擴展值時使用。

以下是用於參數轉換的函數:

  • !singularize
  • !pluralize
  • !uppercase
  • !lowercase
  • !uppercamelcase
  • !lowercamelcase
  • !upperunderscorecase
  • !lowerunderscorecase
  • !upperhyphencase
  • !lowerhyphencase

函數應用於參數,使用以下構造:

<<parameterName> | !functionName>>

如果需要使用多個函數來達到所需轉換效果,則使用豎線符號 (“|”) 將每個函數名稱分隔開,並在每個使用的函數前添加感嘆號 (!)。

例如,給定資源 /foos,其中 <<resourcePathName>> 評估為“foos”,則

  • <<resourcePathName>> ==> “foo”
  • <<resourcePathName>> ==> “FOOS”
  • <<resourcePathName>> ==> “FOO”

並且給定資源 /bars/{barId},其中 resourcePathName>> 評估為“bars”,則

  • <<resourcePathName>> ==> “BARS”
  • <<resourcePathName>> ==> “Bar”

5. Extracting a Resource Type for Collections

Let’s refactor the /foos and /bars resource definitions shown above, using a resource type to capture the common properties. We will use the reserved parameter 6. 提取集合單項資源的類型現在讓我們重點關注 API 中處理集合單項項目的部分:/foos/{fooId}/bars/{barId} 資源。以下是 /foos/{fooId} 的代碼:

/foos:
...
  /{fooId}:
    get:
      description: 獲取一個 Foo
      responses:
        200:
          body:
            application/json:
              type: Foo
        404:
          body:
            application/json:
              type: Error
              example: !include examples/Error.json
    put:
      description: 更新一個 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: 刪除一個 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: 使用此資源類型來表示任何單個項目
    description: 一個 <<typeName>> 類型的單個項
    get:
      description: 獲取一個 <<typeName>> 類型的項
      responses:
        200:
          body:
            application/json:
              type: <<typeName>>
        404:
          body:
            application/json:
              type: Error
              example: !include examples/Error.json
    put:
      description: 更新一個 <<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: 刪除一個 <<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. Traits

Whereas a resource type is used to extract patterns from resource definitions, a trait is used to extract patterns from method definitions that are common across resources.

7.1. Parameters

Along with , one additional reserved parameter is available for use in trait definitions: trait is defined. User-defined parameters may also appear within a trait definition, and where applied, take on the value of the resource in which they are being applied.

7.2. Definition

Notice that the “item” resource type is still full of redundancies. Let’s see how traits can help eliminate them. We’ll start by extracting a trait for any method containing a request body:

traits:
  hasRequestItem:
    body:
      application/json:
        type: <<"typeName">>

Now let’s extract traits for methods whose normal responses contain bodies:

  hasResponseItem:
    responses:
      200:
        body:
          application/json:
            type: <<"typeName">>
  hasResponseCollection:
    responses:
      200:
        body:
          application/json:
            type: <<"typeName">>[]

Finally, here’s a trait for any method that could return a 404 error response:

  hasNotFound:
    responses:
      404:
        body:
          application/json:
            type: Error
            example: !include examples/Error.json

7.3. Application

We then apply this trait to our resource types:

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 <<quot;typeName">>
      is: | [ hasRequestItem: { typeName: <<quot;typeName">> }, hasResponseItem: { typeName: <<quot;typeName">> }, hasNotFound ]
    delete:
      description: Delete a <<quot;typeName">>
      is: [ hasNotFound ]
      responses:
        204:

We can also apply traits to methods defined within resources. This is especially useful for “one-off” scenarios where a resource-method combination matches one or more traits but does not match any defined resource type:

/foos:
...
  /name/{name}:
    get:
      description: List all foos with a certain name
      is: [ hasResponseCollection: { typeName: Foo } ]

8. 結論

在本教程中,我們展示瞭如何顯著減少或,在某些情況下,消除 RAML API 定義中的冗餘。

首先,我們識別了資源中的冗餘部分,認識到它們的模式,並提取資源類型。然後我們對在資源中常見的用於提取特性的方法也進行了相同的操作。然後我們能夠通過將特性應用於我們的資源類型以及“一次性”資源方法組合,這些組合沒有嚴格匹配我們定義的資源類型,從而進一步減少冗餘。

以下是 RAML API 的完整內容:

#%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 } ]
Next »
« Previous
user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.