5 minute read     Posted on:     Updated on:     Views: Loading...

Preface

  OpenAPI Swagger
Picture
Picture Reference https://swagger.io/ https://swagger.io/

OpenAPI 的規範在 2021/02 的時候來到 3.0,而它跟 Swagger 的關係就有點像是 Docker 跟 Open Container Initiative 的關係
也就是說是 Swagger 的開發團隊將他們的規範,貢獻出去給 OpenAPI Initiative
所以如果你看到 Swagger, 可以簡單的把它聯想為 OpenAPI(還是要注意他們的關係)

簡言之,OpenAPI 是文件的規範,Swagger 除了貢獻規範之外它還有其他的工具,這裡就不贅述

Introduction

OpenAPI 3.1.0 §4.3 裡提到

An OpenAPI document MAY be made up of a single document or be divided into multiple,
connected parts at the discretion of the author.

從上述可以得知,OpenAPI 文件是可以進行模組化處理的
不過使用 swagger-ui-express 的時候
需要用一點點的方法才可以 reference 到其他文件

所以這篇 blog 會詳細紀錄如何達到這件事情,以及有哪些坑

Reference Anchor $ref

OpenAPI 有一個我發現很討厭的事情是,它寫起來又臭又長
理所當然的能夠將文件拆分,不僅改寫容易,模組化也有助於 maintain

慶幸的是,OpenAPI 裡面你可以使用 $ref 的關鍵字
根據 OpenAPI 3.1.0 §4.8.23
$ref 可以用於 internal 或是 external reference, 其中 $ref 必須是 URI 的形式(注意到它跟 URL 的差異)

Internal Reference

1
2
3
# main.yaml

$ref: '#/components/schemas/Cat'

要指到同一個文件的 reference 是這樣寫
其中,# 字號代表該文件下的,後綴就代表指向文件內容的 URI

1
2
3
4
5
6
7
8
9
# main.yaml
...

components:
  schemas:
    Dog:
      description: This is a dog
    Cat:
      description: This is a cat

所以它會從 document root 開始找,找到 components 再找到 schema 再找到 Pet
因此上述的 ref 它會被替換成

1
description: This is a pet

External Reference

1
2
# main.yaml
$ref: './pet.yaml#/components/schemas/Cat'
1
2
3
4
5
6
7
# pet.yaml
components:
  schemas:
    Dog:
      description: This is a dog
    Cat:
      description: This is a cat

外部 reference 就是在前面新增檔案位置
所以 main.yaml 經過編譯後會長成這樣

1
description: This is a cat

需要注意 encode 的問題,可參考 JSON pointer

Circular Reference

OpenAPI 3.1.0 的規範中,並沒有實際的提到對於 circular reference 的限制
實際上在測試的時候,它也是允許的,並不會報錯

只是說有一些工具如 Redocly OpenAPI VS Code extension
對於 circular reference 沒辦法正確的識別,會造成套件部份失效
這個就要額外注意

JSON pointer

在使用 $ref 的時候有一點要注意,如果你是 reference 到 endpoint 的時候
該 endpoint url 必須要 encode

RFC 6901

encode 在一般情況下是使用 URL encode, 但是有些字元在 JSON pointer 裡有特殊的意義
根據 RFC 6901 所述

Because the characters ‘~’ (%x7E) and ‘/’ (%x2F) have special
meanings in JSON Pointer, ‘~’ needs to be encoded as ‘~0’ and ‘/’
needs to be encoded as ‘~1’ when these characters appear in a
reference token.

所以當你想要 reference url 的時候要稍微處理一下
比如說 /cat/{catId} 要被 encode 成

1
$ref: "./cat/cat.yaml#/~1cat~1%7BcatId%7D"

其中

  • /~1
  • {%7B
  • }%7D

Node.js Libraries

swagger-jsdoc

swagger-jsdoc 本身可以拿來組合多個 yaml 檔
將它合併成完整的一個 OpenAPI doc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// config.js
import swaggerJSDoc from "swagger-jsdoc";

export const option = swaggerJSDoc({
  definition: {
    openapi: "3.1.0",
    info: {
      title: "Swagger API Test",
      description:
        "This document shows how to use $ref in multiple swagger file",
      version: "1.0.0",
      servers: [
        {
          url: "http://localhost",
        },
      ],
    },
  },
  apis: ["./doc/**/*.yaml"],
});

// doc.js
import express from "express";
import swaggerUi from "swagger-ui-express";

import { option } from "./swagger-jsdoc/config.js";

const uiOption = {
  swaggerOptions: {
    docExpansion: false,
  },
};

const app = express();
app.use("/", swaggerUi.serve, swaggerUi.setup(option, uiOption));
app.listen(3000);

從上述設定檔可以看到,我們指定了 apis: ["./doc/**/*.yaml"]
因此,它會找到所有的文件檔並生成一個新的檔案,所以最後 express 的資料,會是處理過後的
值得一提的是,跨檔案使用 $ref 是沒有效果的
因為它合併成一個檔案了,所以那些 external link 都會找不到

swagger-combine

swagger-combine 也是另外一個可行的選擇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// config.js
import swaggerCombine from "swagger-combine";

export const option = await swaggerCombine("./doc/api.yaml");

// doc.js
import express from "express";
import swaggerUi from "swagger-ui-express";

import { option } from "./swagger-combine/config.js";

const uiOption = {
  swaggerOptions: {
    docExpansion: false,
  },
};

const app = express();
app.use("/", swaggerUi.serve, swaggerUi.setup(option, uiOption));
app.listen(3000);

僅須簡單的將你的進入點丟給它處理,swagger-combine 就會自動 resolve 所有 reference
此外你也不需要像是 swagger-jsdoc 一樣重複定義區塊

Cons

對於 swagger-jsdoc 來說

  1. 跨檔案 $ref 無法使用
  2. 需要額外定義一次 info, servers … 等等的區塊

這裡要稍微提一下,其實你可以在 yaml 那裡單純的從 path 開始寫,info, servers 不需要兩邊寫
只不過如果你有用 redocly 這類的 linter, 針對單純的 yaml 它會報錯
但這部份就看你們的選擇

swagger-combine 的缺點則是

  1. 需要手動引入 endpoint

Implementation Example

上述的例子你可以在 ambersun1234/blog-labs/swagger 當中找到

Work with Docker

上述提到的做法是使用 Node.js 來處理,但是其實不用那麼麻煩
可以使用官方的 Docker image 來處理

1
2
3
4
5
$ docker run --rm -p 8080:8080 \
    --name swagger-api \
    -e SWAGGER_JSON=/swagger/openapi.yml \
    -v $(pwd)/api:/swagger \
    swaggerapi/swagger-ui

這裡會用到 swaggerapi/swagger-ui 這個 image
SWAGGER_JSON 是你的 OpenAPI document root
他不一定要是 json 檔,如上所示這裡是給 yaml 檔

然後他的路徑是 map 到 container 底下的路徑記得不要寫錯
跑起來之後你應該會看到正確的 API doc

每次文件的修改更新都會即時反映在 swagger-ui 上面
只是需要手動 reload 一下

如果你看到 document 的內容是 pet and store
這個代表 swagger 預設的文件,言下之意是你的路徑有錯
記得檢查一下

Create Mock Server with OpenAPI Document

寫文件除了是前後端溝通的橋樑之外,他也可以用來建立 Mock Server
以往的開發流程,前端往往需要依賴後端的實作,這對於開發效率來說是很差的
並且以開發階段來說,兩個部門也不應該有太多的依賴

若是能直接使用 OpenAPI 文件來建立 Mock Server,這樣就可以讓前端開發者不用等待後端的實作
並且能夠有一個真正的後端系統來測試

prism 這套工具可以無痛的幫你建立 Mock Server
僅僅需要簡單的 docker run 即可

prism 本身也會做 auto reload,即時的修改也會即時反映在 mock server 上面

1
2
3
4
$ docker run --init --rm \
  -v $(pwd)/api:/api \
  -p 4010:4010 stoplight/prism:4 \
  mock -h 0.0.0.0 "/api/openapi.yml"

然後你的 API 就會跑在 http://localhost:4010 上面
其中 /api/openapi.yml 是你的 OpenAPI 文件路徑
對於多檔案的情況,他會自己做處理,這個檔案是你的進入點即可

其實你也可以給一個 url,它會自動幫你下載

針對開發不同的 response 他也有相對應的機制
比如官方網站提到的 Modifying Responses

1
2
3
4
5
6
7
8
9
$ curl -v http://127.0.0.1:4010/pets/123 -H "Prefer: code=404"

HTTP/1.1 404 Not Found
content-type: application/json
content-length: 52
Date: Thu, 09 May 2019 15:26:07 GMT
Connection: keep-alive

$ curl -v http://127.0.0.1:4010/pets/123 -H "Prefer: example=exampleKey"

可以透過 -H "Prefer: code=404" 或者是 -H "Prefer: example=exampleKey" 來設定相對應的 response
擁有這些基本的功能,我想對於開發來說已經是足夠的了

Integrate with GitHub Pages

你可以將你的 OpenAPI 文件放在 GitHub Pages 上面
swagger-ui-action 可以自動 build 你的文件
看起來就會像他 README 提到的那樣

1
2
3
4
5
6
7
8
9
10
11
12
13
- name: Generate Swagger UI
  uses: Legion2/swagger-ui-action@v1
  with:
    output: swagger-ui
    spec-file: openapi.json
    GITHUB_TOKEN: $
- name: Copy doc
  # copy other doc files if needed    
- name: Deploy to GitHub Pages
  uses: peaceiris/actions-gh-pages@v3
  with:
    github_token: $
    publish_dir: swagger-ui

如果你的 doc 檔案是跟我一樣用 reference anchor 進行管理的,記得要把其他的檔案 copy 進去

有關 GitHub Action 的討論可以參考 DevOps - 從 GitHub Actions 初探 CI/CD | Shawn Hsu

Nginx as Reverse Proxy

編好的檔案,如果你需要在本機開起來看,記得要用 Nginx 來 reverse proxy
不然你會碰到

1
2
3
Access to fetch at 'file:///xxxx/swagger-config.json' from origin 'null' 
has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes:
http, data, isolated-app, chrome-extension, chrome, https, chrome-untrusted.

老朋友了 CORS(可參考 網頁程式設計三兩事 - 萬惡的 Same Origin 與 CORS | Shawn Hsu)
但解法有點不一樣,這次需要一個 reverse proxy 來幫忙

1
2
3
4
5
6
7
8
9
10
server {
    listen 80;
    server_name swagger-doc;

    # serve index.html
    location / {
        root /app;
        try_files $uri /index.html;
    }
}
1
2
3
4
5
FROM nginx:alpine
COPY . /app
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

把這個 container 跑起來就能夠正確顯示了
記得要 port forward 一下

References

Leave a comment