Swagger/OpenAPI Client Generator for Delphi and FPC

海利鸟發表於2024-09-08

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格式。

根據此參考文字,你可以生成多種語言的客戶端程式碼以訪問該服務。

img

在程式碼生成方面,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單元就足以為我們完成所有繁重的工作。

img

再次感謝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的網頁預覽。

img

此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,,我們會盡力做出適當的安排。


相關文章