Delphi和FPC的Swagger/OpenAPI客戶端生成器 Swagger/OpenAPI Client Generator for Delphi and FPC
Swagger/OpenAPI 是一種用於描述和定義RESTful API的規範和工具集。具體來說,它們提供了以下關鍵特性和作用:
一、定義與背景
- Swagger :最初是一種用於描述RESTful API的規範,它允許開發者以一種標準化的方式定義API的請求、響應、引數、錯誤碼等資訊,使得API的使用和理解變得更加容易。
- OpenAPI :作為Swagger的後繼者,OpenAPI規範由OpenAPI Initiative(OAI)開發和維護。OpenAPI擴充套件了Swagger的功能,提供了更加豐富的API描述能力和更好的相容性。現在,Swagger通常被視為OpenAPI的同義詞,特別是在討論API描述和文件生成時。
二、正文
OpenAPI(前身為Swagger)是一套規範,用於將伺服器API端點的定義編碼為文字,主要是JSON格式。
根據此參考文字,你可以生成多種語言的客戶端程式碼以訪問該服務。
在程式碼生成方面,Delphi似乎遠遠落後於其他語言。對於FPC(Free Pascal Compiler),我更是未找到任何可用的解決方案。
由於我們在Tranquil IT的內部工具中需要此功能,因此我們釋出了新的mormot.net.openapi.pas單元,這可以說是一個改變遊戲規則的舉措。感謝Andreas啟動了這個專案,並在早期階段對其進行了測試!
當前解決方案
在網上快速搜尋關於Delphi和FPC的Swagger或OpenAPI程式碼生成器時,我確實感到失望。
Paolo Rossi為Delphi釋出了一個OpenAPI,但它並不是一個程式碼生成器,而是一個OpenAPI規範解析器和發射器。因此,這並不是我們想要的。
有一個閉源替代方案,但根據我在演示影片中看到的內容,其價格(360歐元!)似乎過高,不予考慮。
Ali Dehbansiahkarbon釋出了他的OpenAPIClientWizard倉庫,該倉庫仍處於測試階段,並且只具有最基本的功能(路徑提取)。
來自TMS的傑出開發者Wagner Landgraf釋出了他的OpenAPI-Delphi-Generator專案,這是Delphi中最先進的嘗試。
但是,它似乎缺少一些基本功能,如allOf支援或適當的錯誤處理。而且,它僅適用於最新版本的Delphi,大量使用泛型,並且生成器依賴於第三方的專有庫。
因此,目前還沒有我們想要的解決方案。
如果你知道在網際網路的某個深處還有另一個被遺漏的庫,請不吝告知你的反饋。
OpenAPI 邂逅 mORMot
事實上,在我們的開源mORMot框架中,我們擁有建立此類客戶端生成器所需的所有工具,尤其是:
- 非常強大的RTTI快取,具有對高階資料結構進行自定義JSON序列化的功能;
- 我們所需的所有JSON解析和文字生成工具,這些工具具有極富表現力的定義(無需繼承類或為類新增冗長的屬性);
- 多個HTTP客戶端類,可在所有支援的平臺和編譯器上執行;
- 一個已經與FPC和Delphi相容的庫,甚至可以與最舊的Delphi版本(如Delphi 7或2007)配合使用,這些版本仍被用於長期存在的生產專案。
由於我們手頭擁有所有這些基本工具,因此僅一個mormot.net.openapi.pas單元就足以為我們完成所有繁重的工作。
再次感謝Tranquil IT IT允許將此工具作為mORMot的一部分發布!
Main Features
以下是我們的Delphi和FPC的OpenAPI程式碼生成器的主要功能:
- 使用高階Pascal記錄和動態陣列表示“物件”DTO(資料傳輸物件)和“陣列”值
- 使用高階Pascal列舉和集合表示“列舉”值
- 將HTTP狀態錯誤程式碼轉換為高階Pascal異常
- 識別相似的“屬性”或“列舉”以重用相同的Pascal型別
- 支援物件、引數或型別的巢狀“$ref”
- 支援“allOf”屬性,具有適當的屬性繼承/過載
- 支援“oneOf”屬性,用於字串或備用記錄型別
- 支援“in”:“header”和“in”:“cookie”引數屬性
- 對於“oneOf”或“anyOf”JSON值,回退到變體Pascal型別
- 每個方法執行都是執行緒安全和阻塞的,以確保安全性
- 生成的原始碼單元非常小且易於使用、閱讀和除錯
- 可以在單元原始碼中生成非常詳細的註釋文件
- 可調引擎,具有大量生成選項(例如,關於詳細程度)
- 利用mORMot的RTTI和JSON核心進行其內部處理
- 與FPC和舊版Delphi(7-2009)相容
- 已經過多個Swagger 2和OpenAPI 3參考內容的測試,但歡迎您提供輸入,因為它並不完全相容!
生成選項確實可以根據您的實際需求調整輸出:
/// 允許自定義TOpenApiParser過程
// - opoNoEnum 禁用任何Pascal列舉型別生成
// - opoNoDateTime 禁用任何Pascal TDate/TDateTime型別生成
// - opoDtoNoDescription 不為DTO生成Description註釋
// - opoDtoNoRefFrom 不為DTO生成'from #/....'註釋
// - opoDtoNoExample 不為DTO生成'Example:'註釋
// - opoDtoNoPattern 不為DTO生成'Pattern:'註釋
// - opoClientExcludeDeprecated 移除任何標記為已棄用的操作
// - opoClientNoDescription 僅為客戶端生成最小注釋
// - opoClientNoException 不會生成任何異常,而是回退到EJsonClient
// - opoClientOnlySummary 將減少操作註釋的詳細程度
// - opoGenerateSingleApiUnit 將使GenerateClient返回一個單獨的{name}.api單元,其中包含所需的DTO和客戶端類
// - opoGenerateStringType 將生成普通字串型別而不是RawUtf8
// - opoGenerateOldDelphiCompatible 將為Delphi 7/2007/2009相容性生成一個void/dummy託管欄位,並避免'T... has no type info'錯誤
// - 例如,參見OPENAPI_CONCISE,用於生成單個單元、簡單且未記錄的輸出
TOpenApiParserOption = ( ...
當然,您的客戶端程式碼中需要一些基本的mORMot單元。該工具不會生成一個“純Delphi RTL”客戶端。但公平地說,早期的Delphi中沒有JSON支援,而且維護編譯器和RTL版本之間的差異,尤其是關於JSON、RTTI、HTTP的差異,最終將像是在重新發明mORMot。我們只需要使用有效的工具。
請注意,生成的客戶端程式碼完全不依賴於mORMot的其他功能,如ORM或SOA。它與這些功能完全分離,儘管這些功能非常強大,但有時也會令人困惑。使用客戶端程式碼時,您將使用mORMot,但幾乎不會注意到它的存在。這隻齧齒動物將躲在它的洞裡。但如果您需要它,例如用於新增日誌或服務,它將很樂意幫助您。😃
Enter the PetStore
最著名的OpenAPI示例是著名的“Pet Store”樣本。
你可以在“petstore.swagger.io”上找到整個API的網頁預覽。
此API在JSON檔案中定義,該檔案可在本要點中找到。
然後我們可以編寫這個小專案:
program OpenApiPetStore;
uses
mormot.core.base,
mormot.core.os,
mormot.net.openapi;
var
p : TOpenApiParser;
begin
p := TOpenApiParser.Create('PetStore'); // 建立OpenAPI解析器例項
try
p.Options := []; // 設定選項為空
p.ParseFile(Executable.ProgramPath + 'petstore.swagger.json'); // 解析指定路徑的swagger.json檔案
p.ExportToDirectory(Executable.ProgramPath); // 匯出解析結果到指定目錄
finally
p.Free; // 釋放解析器例項
end;
end.
注意,如果你更喜歡,很快就會有一個獨立的命令列工具用於生成。
使用預設選項,我們會得到兩個單元,一個包含資料傳輸物件(DTOs),另一個包含實際的客戶端類。
你可以在這個要點gist.中看到結果。
以下只是一個簡單的方法定義:
// getUserByName [get] /user/{username}
//
// 摘要:透過使用者名稱獲取使用者
//
// 引數:
// - [path] Username (required): 需要獲取的使用者名稱。使用user1進行測試。
//
// 響應:
// - 200 (main): 成功操作
// - 400: 提供了無效的使用者名稱
// - 404: 未找到使用者
function GetUserByName(const Username: RawUtf8): TUser;
TUser
記錄將用作方法響應的高階結果記錄。如果mORMot的 RawUtf8=Utf8String
型別不符合你的需求,你可以定義一個選項來生成純 String
值。
你可以觀察到dto單元只有少數依賴項,因此你可以在你的業務邏輯程式碼中使用它,而不會受到客戶端單元的“邏輯汙染”。
實際的DTOs資料結構被定義為記錄,因此它們不需要任何create/free,並且可以輕鬆地使用。一些列舉是從原始Petstore JSON定義中指定的字串值列表中生成的。這使得你的客戶端程式碼非常可讀,並且防錯,因為你無法向伺服器傳送任何未經驗證的值。
客戶端的“魔法”是在客戶端單元中的一個名為 TPetStoreClient
的包裝類中完成的。
每個方法定義都遵循預期的規範,並且具有從原始JSON規範的描述欄位生成的非常準確的註釋。如果你覺得它太冗長,你可以包含 opoClientNoDescription
選項。方法按“標籤”分組,在OpenAPI術語中,這是一種按主題聚集方法的方式。這反映在程式碼順序以及註釋中。
如果我們定義以下選項:
p.Options := OPENAPI_CONCISE;
然後會生成一個幾乎沒有內部文件的單元。
你可以在這個要點中看到這個單元。
// 儲存方法
function GetInventory(): variant;
function PlaceOrder(const Payload: TOrder): TOrder;
function GetOrderById(OrderId: Int64): TOrder;
procedure DeleteOrder(OrderId: Int64);
如果JSON規範沒有像上面的GetInventory()那樣的實際回答佈局,我們無法生成像TOrder這樣的DTO。
因此,我們回退到一個變體,它可以包含伺服器響應的RTTI反序列化後的任何JSON輸入:一個字串、一個整數,或者更可能是一個複雜的物件或陣列,編碼為強大的mORMot TDocVariant自定義變體型別。在未來,如果你更喜歡,我們可以透過適當的選項生成IDocList和IDocDict例項 - 歡迎反饋。
你可能已經注意到,生成的程式碼非常乾淨,尤其是與替代解決方案實際生成的內容相比。它是一個很好的展示,展示瞭如何編寫mORMot程式碼,具有跨平臺RTTI註冊和JSON自定義序列化等高階功能。
More Complex APIs
更復雜的API
在我們的測試和驗證過程中,我們使用了一些更復雜的API定義。
例如,我們內部使用了一個Ultravisor服務,其單檔案API程式碼可以在此處檢視。它包含大量的DTO(資料傳輸物件)和方法。當規範中的多個位置實際元素匹配時,就會生成並重用一些列舉。即使我們沒有在此OPENAPI_CONCISE
中包含所有可用文件(出於安全原因,關於在部落格上釋出有關內部API的詳細資訊),生成的客戶端單元仍然非常易於閱讀。
你可能會注意到,它還定義了一些 Exception
classes,以便生成器能夠將實際的HTTP錯誤程式碼(例如401、403...)對映到真實的Pascal異常 Exception
,並附帶它們自己的結果集DTO。如果API執行成功,其客戶端方法就會按預期執行,並返回輸出值,就像常規的原生代碼一樣。但如果伺服器返回錯誤程式碼,客戶端程式碼就會攔截它,將其對映到設計的異常類 Exception
class,並最終引發異常,同時在其 Error
屬性中包含所有附加資料:
constructor EValidationErrorResponse.CreateResp(const Format: RawUtf8;
const Args: array of const; const Resp: TJsonResponse);
begin
inherited CreateResp(Format, Args, Resp); // 呼叫繼承的構造方法
LoadJson(fError, Resp.Content, TypeInfo(TValidationErrorResponse)); // 載入JSON響應內容到fError
end;
(...)
procedure TUltravisorClient.OnError1(const Sender: IJsonClient;
const Response: TJsonResponse; const ErrorMsg: shortstring);
var
e: EJsonClientClass;
begin
case Response.Status of // 根據響應狀態碼選擇異常類
400:
e := EValidationErrorResponse;
401:
e := EUnauthorizedResponse;
403:
e := EForbiddenResponse;
404:
e := EResourceNotFoundError;
422:
e := EIntegrityErrorResponse;
else
e := EJsonClient;
end;
raise e.CreateResp('%.%', [self, ErrorMsg], Response); // 引發異常,並傳遞響應資訊
end;
這樣,你就可以在客戶端以非常自然的方式處理API錯誤,所有必要的資訊都包含在高階Pascal程式碼中,透過標準的try ... except on E: E#### do ...
塊進行處理。
生成器還會正確記錄HTTP錯誤程式碼和 Exception
classes的對映,例如以下程式碼片段所示:
// post_account_res_add_grant_auth [post] /accounts/{uuid}/add-grant-auth/
//
// 摘要:授予使用者對帳戶進行授權許可的許可權
// 描述:
// 角色:對於vm物件為vm_admin,否則為templates
//
// 引數:
// - [path] Uuid (必需):管理程式uuid
// - [body] Payload (必需)
//
// 響應:
// - 200 (main):成功
// - 400 [EValidationErrorResponse]:引數格式或型別無效
// - 401 [EUnauthorizedResponse]:使用者未認證
// - 403 [EForbiddenResponse]:使用者許可權不足
// - 404 [EResourceNotFoundError]:未找到管理程式
// - 422 [EIntegrityErrorResponse]:引數格式有效但與伺服器狀態不相容
function PostAccountResAddGrantAuth(const Uuid: RawUtf8; const Payload: TUserShort): TDbAccount;
另一個API示例來自Paolo Rossi的參考材料。你可以在這個要點gist中找到其JSON規範、DTO和客戶端單元,以及其單個API單元。
以下是生成的一個方法及其實現的摘錄:
// sign_delete [delete] /scope/{job}
//
// 描述:
// 刪除一個驗證作業
//
// 引數:
// - [path] Job (必需):作業ID(20個字元)
//
// 響應:
// - 200 (main):成功刪除
// - 404 [EError]:未找到作業 `unknown-job`
// - default [EError]
function SignDelete(const Job: RawUtf8): TDtoAuth14;
(...)
function TAuthClient.SignDelete(const Job: RawUtf8): TDtoAuth14;
begin
fClient.Request('DELETE', '/scope/%', [Job], [], [],
result, TypeInfo(TDtoAuth14), OnError1); // 傳送DELETE請求,並處理響應或錯誤
end;
這是我們程式碼生成器的一個很好的示例,它利用了mORMot的核心功能。而且這段程式碼可以在非常老的Delphi 7上執行!
Feedback is Welcome
當然,我們並沒有完全實現所有OpenAPI v3.1規範。因此,如果你對這個生成器有任何問題,歡迎在我們的論壇上報告你的問題report your problematic JSON on our forum,,我們會盡力做出適當的安排。