iT邦幫忙

2024 iThome 鐵人賽

DAY 12
0
IT 管理

我要成為文件工程師── Web API 文件撰寫系列 第 12

[Day 12] TypeSpec:撰寫 OpenAPI 的領域特定語言 (六)

  • 分享至 

  • xImage
  •  

模型

多型

接下來將使用在 [Day 06] OpenAPI:標準的 API 描述技術規範(五) 時相同的情境做說明。會員允許多種登入方式,而透過不同的登入方式會有不同的註冊資訊組合,包含:以使用者名稱和密碼註冊(有使用者名稱和密碼兩個欄位)、以 Email 和密碼註冊(有 Email 和密碼兩個欄位)、和使用三方登入(只有一個外部 ID)。

若要宣告一個欄位有多種型別,可使用 | 串接它的各個型別。先定義三種不同的帳號模型,範例程式碼如下:

// member/model/default-account-model.tsp
namespace MemberService.Member.Model;

model DefaultAccountModel {
    @doc("帳號")
    userId: string;

    @doc("密碼")
    password: string;
}

// member/model/email-account-model.tsp
namespace MemberService.Member.Model;

model EmailAccountModel {
    @doc("電子郵件")
    email: string;

    @doc("密碼")
    password: string;
}

// member/model/thirdparty-account-model.tsp
namespace MemberService.Member.Model;

model ThirdpartyAccountModel {
    @doc("第三方使用者 ID")
    userId: string;
}

接著在 MemberModelaccount 欄位用 | 串接型別,範例程式碼如下:

// member/model/member-model.tsp
import "./default-account-model.tsp";
import "./email-account-model.tsp";
import "./thirdparty-account-model.tsp";

namespace MemberService.Member.Model;

model MemberModel {
    @doc("會員 ID")
    id: int64;

    @doc("帳號")
    account: DefaultAccountModel | EmailAccountModel | ThirdpartyAccountModel;

    // 中間省略
}

這時編譯後得到的 openapi.yaml 中可看到 account 欄位用 anyOf 對應到三個模型如下:

# 以上省略
components:
  schemas:
    Member.Model.MemberModel:
      type: object
      required:
        - id
        - account
        - name
        - phone
      properties:
        id:
          type: integer
          format: int64
          description: 會員 ID
        account:
          anyOf:
            - $ref: '#/components/schemas/Member.Model.DefaultAccountModel'
            - $ref: '#/components/schemas/Member.Model.EmailAccountModel'
            - $ref: '#/components/schemas/Member.Model.ThirdpartyAccountModel'
          description: 帳號
# 以下省略

繼承

一種常見的 API 設計是所有 API 的 Response 都有相同的基本結構,僅在特定欄位視 API 不同而有不同的回傳,例如下面的 JSON:

{
  "message": "Success",
  "data": {} // data 欄位因 API 而不同
}

透過繼承的方式,可避免每支 API 的回傳模型都要自己定義一次基礎結構的問題。首先先定義出回傳的基礎結構模型,範例程式碼如下:

// base/model/base-response-model.tsp
import "@typespec/http";

using TypeSpec.Http;

namespace MemberService.Base.Model;

model BaseResponseModel {
    @statusCode _: 200;
    @header `x-request-id`: string;

    @doc("回應訊息")
    message: string;
}

接著使用 extend 關鍵字定義取得單一會員 API 回傳模型繼承 BaseResponseModel,並針對這支 API 的場景回傳型別為 MemberModeldata 欄位,範例程式碼如下:

// member/model/get-member-response-model.tsp
import "../../base/model/base-response-model.tsp";
import "../model/member-model.tsp";

using MemberService.Base.Model;

namespace MemberService.Member.Model;

model GetMemberResponseModel extends BaseResponseModel {
    @doc("回應資料")
    data: MemberModel;
}

並將操作的回傳型別改為 GetMemberResponseModel,範例程式碼如下:

// member/member.tsp
import "@typespec/http";
import "./model/member-model.tsp";
import "./model/get-member-response-model.tsp";
import "../base/model/base-response-model.tsp";

using TypeSpec.Http;
using MemberService.Member.Model;
using MemberService.Base.Model;

namespace MemberService.Member;

@summary("取得單一會員")
@doc("""
    這個 API 用來取得單一會員的資料。
    這個 API 需要提供會員 ID,並回應該會員的基本資料。
""")
@get()
@route("members/{memberId}")
op get(@path memberId: int64): GetMemberResponseModel;

// 以下省略

這時編譯後得到的 openapi.yaml 中可看到 GetMemberResponseModelallOf 對應到 BaseResponseModelMemberModel 兩個模型如下:

components:
  schemas:
    Base.Model.BaseResponseModel:
      type: object
      required:
        - message
      properties:
        message:
          type: string
          description: 回應訊息
    Member.Model.GetMemberResponseModel:
      type: object
      required:
        - data
      properties:
        data:
          allOf:
            - $ref: '#/components/schemas/Member.Model.MemberModel'
          description: 回應資料
      allOf:
        - $ref: '#/components/schemas/Base.Model.BaseResponseModel'

泛型

延續繼承的範例,比較常見的寫法是使用泛型,指定型別為 <T> 並在各個地方傳入的型別。調整 BaseResponseModel 支援泛型欄位 data,並命名資料型別為 DataType,範例程式碼如下:

// member/model/get-member-response-model.tsp
import "@typespec/http";

using TypeSpec.Http;

namespace MemberService.Base.Model;

model BaseResponseModel<DataType> {
    @statusCode _: 200;
    @header `x-request-id`: string;

    @doc("回應訊息")
    message: string;

    @doc("回應資料")
    data: DataType;
}

接著調整操作的回傳型別為 BaseResponseModel<MemberModel>,範例程式碼如下:

// member/member.tsp
import "@typespec/http";
import "./model/member-model.tsp";
import "../base/model/base-response-model.tsp";

using TypeSpec.Http;
using MemberService.Member.Model;
using MemberService.Base.Model;

namespace MemberService.Member;

@summary("取得單一會員")
@doc("""
    這個 API 用來取得單一會員的資料。
    這個 API 需要提供會員 ID,並回應該會員的基本資料。
""")
@get()
@route("members/{memberId}")
op get(@path memberId: int64): BaseResponseModel<MemberModel>;

// 以下省略

這時編譯後得到的 openapi.yaml 中可看到 responses.200 即為 BaseResponseModel 結構,並在 data 欄位使用 MemberModel 如下:

paths:
  /members/{memberId}:
    get:
      operationId: Member_get
      summary: 取得單一會員
      description: |2-
            這個 API 用來取得單一會員的資料。
            這個 API 需要提供會員 ID,並回應該會員的基本資料。
      parameters:
        - name: memberId
          in: path
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: The request has succeeded.
          headers:
            x-request-id:
              required: true
              schema:
                type: string
          content:
            application/json:
              schema:
                type: object
                required:
                  - message
                  - data
                properties:
                  message:
                    type: string
                    description: 回應訊息
                  data:
                    allOf:
                      - $ref: '#/components/schemas/Member.Model.MemberModel'
                    description: 回應資料

明天將介紹附加屬性及 TypeSpec.Http 提供的模型。


上一篇
[Day 11] TypeSpec:撰寫 OpenAPI 的領域特定語言 (五)
系列文
我要成為文件工程師── Web API 文件撰寫12
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言