1. 概述
在我们之前的 RAML 教程 中,我们介绍了 RESTful API Modeling Language,并基于一个名为 Foo 的实体创建了一个简单的 API 定义。现在设想一个现实中的 API,其中包含多个实体类型的资源,它们都具有相同或类似的 GET、POST、PUT 和 DELETE 操作。你很快就会发现,API 文档变得冗长且重复。
本文将展示如何通过 RAML 的 resource types 和 traits 特性来 消除资源和方法定义中的冗余,通过提取和参数化通用部分,减少复制粘贴错误,并使 API 定义更加简洁。
2. 我们的 API
为了演示 resource types 和 traits 的优势,我们将扩展原始 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 中的资源列表时,我们可以识别出一些重复模式。例如:
- 单个实体的增删改查(CRUD)操作具有相似的 URI 和方法结构;
- 获取实体集合的操作也有类似的模式。
这种“集合与单个项”的模式是 RAML 中提取 resource types 最常见的场景之一。
让我们来看一段代码片段:
注:以下代码中,仅包含
...
的行表示省略部分内容以保持简洁。
/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
通过对比 /foos 和 /bars 的 RAML 定义,包括所使用的 HTTP 方法,我们可以发现许多重复的属性,再次印证了模式的存在。
一旦在资源或方法定义中发现了模式,就可以考虑使用 RAML 的 resource type 或 trait 来优化。
4. 资源类型(Resource Types)
要实现 API 中的模式,resource types 使用双尖括号(<<
和 >>
)包围的保留参数和用户自定义参数。
4.1. 保留参数
有两个保留参数可以在资源类型定义中使用:
<<resourcePath>>
:表示完整的 URI(不包括baseURI
);<<resourcePathName>>
:表示 URI 中最右边的斜杠后的部分,忽略花括号{}
。
例如:
- 对于资源
/foos
,<<resourcePath>>
为/foos
,<<resourcePathName>>
为foos
; - 对于资源
/foos/{fooId}
,<<resourcePath>>
为/foos/{fooId}
,<<resourcePathName>>
为foos
。
4.2. 用户自定义参数
资源类型定义中也可以包含用户自定义参数。与保留参数不同,这些参数的值必须在使用资源类型时显式指定,且不能更改。
虽然可以在定义开头声明这些参数,但这不是必须的,也不是常见做法,因为根据参数名和使用上下文通常可以推断其用途。
4.3. 参数函数
在参数中可以使用以下文本处理函数:
- !singularize
- !pluralize
- !uppercase
- !lowercase
- !uppercamelcase
- !lowercamelcase
- !upperunderscorecase
- !lowerunderscorecase
- !upperhyphencase
- !lowerhyphencase
使用方式如下:
<<parameterName | !functionName>>
多个函数可以通过 |
分隔,并在每个函数前加上 !
:
例如,对于资源 /foos
,其中 <<resourcePathName>>
为 foos
:
<<resourcePathName | !singularize>>
➜"foo"
<<resourcePathName | !uppercase>>
➜"FOOS"
<<resourcePathName | !singularize | !uppercase>>
➜"FOO"
对于资源 /bars/{barId}
,其中 <<resourcePathName>>
为 bars
:
<<resourcePathName | !uppercase>>
➜"BARS"
<<resourcePathName | !uppercamelcase>>
➜"Bar"
5. 提取集合资源类型
我们重构 /foos 和 /bars 的资源定义,使用 resource type 捕获它们的通用部分。我们使用保留参数 <<resourcePathName>>
和用户自定义参数 <<typeName>>
来表示数据类型。
5.1. 定义
下面是一个表示集合的 resource type 定义:
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>>
由于我们的数据类型只是资源名称的首字母大写单数形式,我们也可以通过函数处理 <<resourcePathName>>
来替代 <<typeName>>
参数:
resourceTypes:
collection:
...
get:
...
type: <<resourcePathName|!singularize|!uppercamelcase>>[]
post:
...
type: <<resourcePathName|!singularize|!uppercamelcase>>
5.2. 应用
使用上述定义,我们可以通过如下方式应用 collection
资源类型:
/foos:
type: { collection: { "typeName": "Foo" } }
get:
queryParameters:
name?: string
ownerName?: string
...
/bars:
type: { collection: { "typeName": "Bar" } }
注意,我们仍然可以保留两个资源之间的差异(如 queryParameters
),同时充分利用资源类型提供的通用结构。
6. 提取单个项资源类型
现在我们关注 API 中处理单个项的部分:/foos/{fooId}
和 /bars/{barId}
。
6.1. 定义
下面是处理单个项的 resource type 定义:
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. 特性(Traits)
与 resource type 用于提取资源定义中的模式不同,trait 用于提取方法定义中的通用模式。
7.1. 参数
除了 <<resourcePath>>
和 <<resourcePathName>>
外,trait 中还可以使用 <<methodName>>
参数,表示当前方法的 HTTP 动词(如 GET、POST 等)。用户自定义参数同样可以在 trait 中使用。
7.2. 定义
我们提取一些通用的 trait 来简化方法定义:
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
7.3. 应用
然后将这些 trait 应用于 resource type 中:
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:
我们也可以将 trait 应用于资源中的方法,特别适用于“一次性”场景:
/foos:
...
/name/{name}:
get:
description: List all foos with a certain name
is: [ hasResponseCollection: { typeName: Foo } ]
8. 总结
在本教程中,我们展示了如何通过 resource types 和 traits 显著减少甚至消除 RAML API 定义中的冗余。
✅ 我们首先识别资源中的重复部分,提取出 resource types;
✅ 然后对方法中的通用结构提取 traits;
✅ 最后通过将 traits 应用于 resource types 和个别方法,进一步消除冗余。
下面是最终的完整 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 } ]