最近在研究 oapi-codegen 這套基於 OpenAPI 3.0 自動生成 Go boilerplate 程式的工具,能幫助開發者省下很多撰寫實現 HTTP Server 端口、marshalling 和 unmarshalling 的重複程式碼的時間。當進行系統重構或使用像是文件先行(Documentation-Driven Development)的開發方法時也很適用。
在此紀錄一下餵入 OpenAPI 檔案時遇到的問題。
問題概述使用以下 OpenAPI yaml 檔(擷取片段)
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
37
38
39
40
41
42
43
44
45
schemas :
Response :
status :
description : 狀態
type : integer
example : 200
message :
description : 訊息
type : string
example : success
detail :
description : 詳細訊息
type : object
example : {}
#...more
paths :
/iot/stations :
get :
parameters :
- name : uniqid
in : query
description : Uniqid
required : false
schema :
type : string
responses :
'200' :
description : OK
content :
application/json :
schema :
type : object
properties :
data :
type : object
properties :
items :
type : array
#...more
message :
$ref : '#/components/schemas/Response/message'
status :
$ref : '#/components/schemas/Response/status'
跑生成代碼的指令時
1
$ oapi-codegen -package main openapi.yaml > openapi.gen.go
會噴這個錯誤
error generating code: error creating operation definitions: error generating response definitions: error generating request body definition: error generating Go schema for property 'message': error turning reference (#/components/schemas/Response/message) into a Go type: unexpected reference depth: 5 for ref: #/components/schemas/Response/message local: true
看了一下原始碼 ,發現他只能解析四層的 $ref path:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// https://github.com/deepmap/oapi-codegen/blob/master/pkg/codegen/utils.go#L322-L332
// refPathToGoType returns the Go typename for refPath given its
func refPathToGoType ( refPath string , local bool ) ( string , error ) {
if refPath [ 0 ] == '#' {
pathParts := strings . Split ( refPath , "/" )
depth := len ( pathParts )
if local {
if depth != 4 { // <---------here!
return "" , fmt . Errorf ( "unexpected reference depth: %d for ref: %s local: %t" , depth , refPath , local )
}
} else if depth != 4 && depth != 2 {
return "" , fmt . Errorf ( "unexpected reference depth: %d for ref: %s local: %t" , depth , refPath , local )
}
也就是說,只有長這樣的 path 才能被順利解析:
#/components/responses/Baz
而在上面的 yaml 當中的 path 已經到第五層 #/components/schemas/Response/message
,因此會報錯。
後來發現,這種寫法不是合法的 OpenAPI 格式的寫法(但用 redoc 可以 build 出來),因為當跑 lint 的時候會報錯。
如果要改成合法的格式,首先 schema 的地方要在每個 Object 底下增加 properties
屬性:
1
2
3
4
5
6
7
schemas :
Response :
properties : # <-------- add this
status :
description : 狀態
type : integer
example : 200
然後 $ref path 要改成 #/components/schemas/Response/properties/message
才能通過 linter 檢查。
但即使改成了合法格式,在用 oapi-codegen 生成代碼時還是會報錯。因為 path 總層數變成 6 層,依然無法被解析。
解決方法
1. 改成只使用四層的 $ref像這樣:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
schemas :
Resource :
type : object
properties :
data :
type : object
properties :
items :
type : array
#...more
message :
#...more
status :
#...more
paths :
#...more
responses :
'200' :
description : OK
content :
application/json :
schema :
$ref : '#/components/schemas/Resource'
但這樣一來就變得很不彈性,在幾乎沒什麼 $ref 會引用到相同內容的情況下,還不如直接 inline,而且要改的地方有點太多了(懶)
2. 先把檔案修正成可以通過 OpenAPI 的 linter 檢查,然後再用工具 取代掉 $ref 的部分想說反正只是為了能夠使用代碼生成工具,不想花太多精力大改原本的 OpenAPI 文件,於是最後採取了這個方法。
剛剛有稍微提到原本的格式跑 lint 時會有錯誤,需要進行修正,只要能夠成功跑過 linter 的檢查,就可以再用工具把 $ref 引用的地方全都取代成相對應的定義內容。這篇問答 有提供兩種工具都能達成這個目的,在這邊我們選用了 redockly-cli (沒有為什麼只是我們也有用 redoc)。
首先修正上面剛剛提到的 properties 和 $ref 的部分後,就可以成功跑過 linter:
1
$ redocly lint openapi.yaml
warning 的部分不用理他。
然後再對檔案跑 bundle 指令:
1
$ redocly bundle --dereferenced openapi.yaml --output openapi_0317.yaml
然後再一次跑 oapi-codegen 指令:
1
$ oapi-codegen -package ports openapi_0317.yaml > openapi.gen.go
總算成功 gen 出檔案啦🎉
Note在預設不帶任何 -generate
參數的情況下,該指令會幫我們產生所有包含 server, types, client, spec 等等相關程式碼在指定輸出的檔案裡,且預設使用的 server 是 Echo。(詳細說明參考文件 )
假設我們的需求是使用 gin ,並且只需要 server 和 types 部分的程式碼
server
:生成 server 接口以及相關的程式碼,包括將參數綁定到 struct、參數類型錯誤 throw Exception 的處理等等types
:定義在 OpenAPI components 當中的類型(struct),包含 request params/body 和 response body
並且分別放置於不同檔案中,可以這樣做:
先產生 gin server 的檔案
1
$ oapi-codegen -package ports -generate gin openapi_0317.yaml > openapi_api.gen.go
再產生 types 的檔案
1
$ oapi-codegen -package ports -generate types openapi_0317.yaml > openapi_types.gen.go