這篇文章是系列的一部分
• 介紹 RAML – RESTful API 建模語言
• 使用資源類型和特性的 RAML 中消除冗餘 ((當前文章))
• 使用包含、庫、覆蓋和擴展模塊化 RAML
• 使用註釋定義自定義 RAML 屬性
• 使用註釋定義自定義 RAML 屬性
在我們的RAML教程文章中,我們介紹了RESTful API建模語言並基於一個名為Foo的單實體創建了一個簡單的API定義。現在想象一個真實世界的API,其中您擁有多個實體類型的資源,所有這些資源都具有相同的或相似的GET、POST、PUT和DELETE操作。您可以看到您的API文檔會迅速變得冗長和重複。
在本文中,我們展示了RAML中資源類型和特性功能的用法,如何通過提取和參數化常見的部分來消除資源和方法定義中的冗餘,從而在避免複製粘貼錯誤的同時,使您的API定義更加簡潔。
為了展示資源類型和特性的好處,我們將擴展我們原始的API,添加第二個實體類型的資源,即Bar。以下是構成我們修訂後的API的資源:
隨着我們瀏覽 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 的 資源類型或特性。
為了實現API中發現的模式,resource types 使用預留和用户自定義的參數,這些參數被雙引號包圍 (<< 和 >>)。
兩個預留參數可用於resource type定義:
一個resource type 定義也可能包含用户自定義的參數。與預留參數不同,其值是根據正在定義的資源動態計算的,而用户自定義參數的值必須在resource type 包含它們的地方賦值,並且這些值不會改變。
用户自定義參數可以在resource type 定義的開頭聲明,儘管這樣做並非必需,也不常見,因為讀者通常可以根據參數的名稱和它們被使用的上下文來推斷其預期用途。
有若干個有用的文本函數可用於在參數被用於resource definition中轉換參數擴展值時使用。
以下是用於參數轉換的函數:
函數應用於參數,使用以下構造:
<<parameterName> | !functionName>>
如果需要使用多個函數來達到所需轉換效果,則使用豎線符號 (“|”) 將每個函數名稱分隔開,並在每個使用的函數前添加感嘆號 (!)。
例如,給定資源 /foos,其中 <<resourcePathName>> 評估為“foos”,則
並且給定資源 /bars/{barId},其中 resourcePathName>> 評估為“bars”,則
/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”字符串以及它們各自的複數形式和/或大寫形式的出現。
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
/foos:
...
/{fooId}:
type: { item: { "typeName": "Foo" } }
...
/bars:
...
/{barId}:
type: { item: { "typeName": "Bar" } }
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.
Along with , one additional reserved parameter is available for use in trait definitions: trait
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
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 } ]
首先,我們識別了資源中的冗餘部分,認識到它們的模式,並提取資源類型。然後我們對在資源中常見的用於提取特性的方法也進行了相同的操作。然後我們能夠通過將特性應用於我們的資源類型以及“一次性”資源方法組合,這些組合沒有嚴格匹配我們定義的資源類型,從而進一步減少冗餘。
以下是 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 } ]