1. 概述

在我们之前的 RAML 教程 中,我们介绍了 RESTful API Modeling Language,并基于一个名为 Foo 的实体创建了一个简单的 API 定义。现在设想一个现实中的 API,其中包含多个实体类型的资源,它们都具有相同或类似的 GET、POST、PUT 和 DELETE 操作。你很快就会发现,API 文档变得冗长且重复。

本文将展示如何通过 RAML 的 resource typestraits 特性来 消除资源和方法定义中的冗余,通过提取和参数化通用部分,减少复制粘贴错误,并使 API 定义更加简洁。

2. 我们的 API

为了演示 resource typestraits 的优势,我们将扩展原始 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 typetrait 来优化。

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 typestraits 显著减少甚至消除 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 } ]

原始标题:Simplify RAML with Resource Types and Traits | Baeldung