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;
usesmormot.core.base,mormot.core.os,mormot.net.openapi;varp : TOpenApiParser;
beginp := TOpenApiParser.Create('PetStore'); // 创建OpenAPI解析器实例tryp.Options := []; // 设置选项为空p.ParseFile(Executable.ProgramPath + 'petstore.swagger.json'); // 解析指定路径的swagger.json文件p.ExportToDirectory(Executable.ProgramPath); // 导出解析结果到指定目录finallyp.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);
begininherited 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);
vare: EJsonClientClass;
begincase Response.Status of // 根据响应状态码选择异常类400:e := EValidationErrorResponse;401:e := EUnauthorizedResponse;403:e := EForbiddenResponse;404:e := EResourceNotFoundError;422:e := EIntegrityErrorResponse;elsee := 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;
beginfClient.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,,我们会尽力做出适当的安排。