如何模組化 Swagger / OpenAPI Specification 檔案

當我們的 Swagger / OpenAPI Specification 寫到某一個程度就會開始想要模組化、重用它,例如,當 components/schemas 節點需要被其他的檔案參照。

開發環境

  • Windows 11
  • Rider

分離檔案

延續上篇我還是選用 OpenAPI Specification 3.0 的範例 OpenAPI-Specification/petstore.yaml,內容如下

openapi: "3.0.0"
info:
  version: 1.0.0
  title: Swagger Petstore
  license:
    name: MIT
servers:
  - url: http://petstore.swagger.io/v1
paths:
  /pets:
    get:
      summary: List all pets
      operationId: listPets
      tags:
        - pets
      parameters:
        - name: limit
          in: query
          description: How many items to return at one time (max 100)
          required: false
          schema:
            type: integer
            format: int32
      responses:
        '200':
          description: A paged array of pets
          headers:
            x-next:
              description: A link to the next page of responses
              schema:
                type: string
          content:
            application/json:    
              schema:
                $ref: "#/components/schemas/Pets"
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    post:
      summary: Create a pet
      operationId: createPets
      tags:
        - pets
      responses:
        '201':
          description: Null response
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /pets/{petId}:
    get:
      summary: Info for a specific pet
      operationId: showPetById
      tags:
        - pets
      parameters:
        - name: petId
          in: path
          required: true
          description: The id of the pet to retrieve
          schema:
            type: string
      responses:
        '200':
          description: Expected response to a valid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Pet"
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
components:
  schemas:
    Pet:
      type: object
      required:
        - id
        - name
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
        tag:
          type: string
    Pets:
      type: array
      items:
        $ref: "#/components/schemas/Pet"
    Error:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: integer
          format: int32
        message:
          type: string

 

分離 components / parameters

根據結構名稱建立 components 資料夾和 schemas.yaml,然後在方案資料夾建立相同的資料夾結構

 

@schemas.yaml

Pet:
  type: object
  required:
    - id
    - name
  properties:
    id:
      type: integer
      format: int64
    name:
      type: string
    tag:
      type: string
        
Pets:
  type: array
  items:
    $ref: "#/Pet"
      
Error:
  type: object
  required:
    - code
    - message
  properties:
    code:
      type: integer
      format: int32
    message:
      type: string

 

@index.yaml

在 request/response 節點使用 $ref: path

  • 原本:$ref: "#/components/schemas/Pets"
  • 現在:$ref: "components/schemas.yaml#/Pets"

Components 節點的使用方式參考:Components Object (swagger.io)

openapi: "3.0.3"
info:
  version: 1.0.0
  title: Swagger Petstore
  license:
    name: MIT
servers:
  - url: http://localhost:7087/api/
#  - url: http://petstore.swagger.io/v1
paths:
  /pets:
    get:
      summary: List all pets
      operationId: listPets
      tags:
        - pets
      parameters:
        - name: limit
          in: query
          description: How many items to return at one time (max 100)
          required: false
          schema:
            type: integer
            format: int32
      responses:
        '200':
          description: A paged array of pets
          headers:
            x-next:
              description: A link to the next page of responses
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "components/schemas.yaml#/Pets"
        
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: 'components/schemas.yaml#/Error'
    post:
      summary: Create a pet
      operationId: createPets
      tags:
        - pets
      responses:
        '201':
          description: Null response
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: "components/schemas.yaml#/Error"
  /pets/{petId}:
    get:
      summary: Info for a specific pet
      operationId: showPetById
      tags:
        - pets
      parameters:
        - $ref: "components/parameters.yaml#/petId"
      responses:
        '200':
          description: Expected response to a valid request
          content:
            application/json:
              schema:
                $ref: "components/schemas.yaml#/Pet"
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: "components/schemas.yaml#/Error"

 

分離節點的技巧就大同小異,這裡就不贅述了

需要知道更多細節的可以參考以下連結

如何撰寫立即可交付的組件化 Swagger 文件. 雖然公司的部分專案已經使用 Swagger 一段時間,但我總覺得… | by Leo Chien | Medium

How to split a large OpenAPI document into multiple files - David Garcia

合併檔案

由於檔案被我分離了原本的 Code Gen 也會因為找不到外部節點而失效,這時候還需要進行檔案合併的動作,市面上有許多的 Code Gen 工具都有支援,我研究了好幾套,最終,選定了openapi-merger

安裝

npm install -g openapi-merger

 

 合併檔案 

openapi-merger -i openapi.yaml -o merged.yaml

 

新增 merge 資料夾,和 index.yaml,且在方案總管建立對應資料夾和加入檔案,確定指令沒有問題

openapi-merger -i ./doc/index.yaml -o ./doc/merge/index.yaml

 

當指令碼確定成功後,便收納到 taskfile,由 spec-codegen 集中執行,他的順序為

  1. 合併檔案
  2. 產生 Client Code
  3. 產生 Server Code
version: "3"

dotenv: [ "secrets/secrets.env" ]

tasks:
  spec-codegen:
    desc: 產生 Client / Server Code
    cmds:
      - task: spec-merge-file
      - task: spec-codegen-client
      - task: spec-codegen-server
        
  spec-codegen-client:
    desc: 產生 Client Code
    cmds:
      - nswag openapi2csclient /input:doc/merge/index.yaml /classname:LabSpecClient /namespace:Lab.SpecFirst.Adapter /output:src/Lab.SpecFirst.Adapter/AutoGenerated/LabSpecClient.cs /jsonLibrary:SystemTextJson /generateClientInterfaces:true /exposeJsonSerializerSettings:false /useBaseUrl:false

  spec-codegen-server:
    desc: 產生 Server Code
    cmds:
      - nswag openapi2cscontroller /input:doc/merge/index.yaml /classname:SpecFirstContract /namespace:Lab.SpecFirst.Web.Controllers /output:src/Lab.SpecFirst.Web/AutoGenerated/Controller.cs /jsonLibrary:SystemTextJson 
  
  spec-merge-file:
    desc: 合併 Swagger File
    cmds:
      - openapi-merger -i ./doc/index.yaml -o ./doc/merge/index.yaml

 

執行結果如下:

 

範例位置

sample.dotblog/WebAPI/Swagger/Lab.SpecFirst2 at master · yaochangyu/sample.dotblog (github.com)

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo