Current Solutions
A quick Internet search about Delphi and FPC code generator for swaggers or OpenAPI did disappoint me.
Paolo Rossi published an Open API for Delphi, which is not a code generator, but an OpenAPI specs parser and emitter. So not what we want here.
There is a closed source alternative, which seemed too highly priced (360€!) to be considered, in respect to what I saw in the presentation video.
Ali Dehbansiahkarbon published his OpenAPIClientWizard repository which is still in beta and has only the most basic set of features (path extraction).
The great Wagner Landgraf from TMS published his OpenAPI-Delphi-Generator project, which is the most advanced attempt in Delphi.
But it seems to lack some basic features like allOf
support, or proper error handling. And it is for recent Delphi versions only, heavily using generics, and the generator depends on a proprietary third party library.
So, currently nothing as we would like to have.
Don't hesitate to give your feedback, if another library was lost in the deep corners of the Internet.
OpenAPI meets the mORMot
In fact, in our Open Source mORMot framework, we have all the tools we need to create such a client generator, especially:
- very powerful RTTI cache, with custom JSON serialization of high-level data structure;
- all the JSON parsing and text generation tools we need, with very expressive definitions (no need to inherit or add verbose attributes to classes);
- several HTTP client classes, working on all support platforms and compilers;
- a library already working with FPC and Delphi, even with oldest Delphi revisions like Delphi 7 or 2007, which are still used on production for long-time-existing projects.
Since we had all those basic tools at hand, a single mormot.net.openapi.pas unit was enough to make all the hard work for us.
Thanks again Tranquil IT to allow publishing this tool as part of mORMot!
Main Features
Here are the top features of our OpenAPI code generator for Delphi and FPC:
- Use high-level pascal records and dynamic arrays for "object" DTOs and "array" values
- Use high-level pascal enumerations and sets for "enum" values
- Translate HTTP status error codes into high-level pascal Exceptions
- Recognize similar "properties" or "enum" to reuse the same pascal type
- Support of nested "$ref" for objects, parameters or types
- Support "allOf" attribute, with proper properties inheritance/overloading
- Support "oneOf" attribute, for strings or alternate record types
- Support of "in":"header" and "in":"cookie" parameter attributes
- Fallback to
variant
pascal type for "oneOf" or "anyOf" JSON values - Each method execution is thread-safe and blocking, for safety
- Generated source code units are very small and easy to use, read and debug
- Can generate very detailed comment documentation in the unit source code
- Tunable engine, with plenty of generation options (e.g. about verbosity)
- Leverage the mORMot RTTI and JSON kernel for its internal plumbing
- Compatible with FPC and oldest Delphi (7-2009)
- Tested with several Swagger 2 and OpenAPI 3 reference content, but your input is welcome, because it is not fully compliant!
The generation options can indeed adapt the output to your actual needs:
/// allow to customize TOpenApiParser process // - opoNoEnum disables any pascal enumeration type generation // - opoNoDateTime disables any pascal TDate/TDateTime type generation // - opoDtoNoDescription generates no Description comment for the DTOs // - opoDtoNoRefFrom generates no 'from #/....' comment for the DTOs // - opoDtoNoExample generates no 'Example:' comment for the DTOs // - opoDtoNoPattern generates no 'Pattern:' comment for the DTOs // - opoClientExcludeDeprecated removes any operation marked as deprecated // - opoClientNoDescription generates only the minimal comments for the client // - opoClientNoException won't generate any exception, and fallback to EJsonClient // - opoClientOnlySummary will reduce the verbosity of operation comments // - opoGenerateSingleApiUnit will let GenerateClient return a single // {name}.api unit, containing both the needed DTOs and the client class // - opoGenerateStringType will generate plain string types instead of RawUtf8 // - opoGenerateOldDelphiCompatible will generate a void/dummy managed field for // Delphi 7/2007/2009 compatibility and avoid 'T... has no type info' errors // - see e.g. OPENAPI_CONCISE for a single unit, simple and undocumented output TOpenApiParserOption = ( ...
Of course, you would need some basic mORMot units in your client code. The tool does not generate a "pure Delphi RTL" client. But to be fair, there was no JSON support in early Delphi, and maintaining the differences between versions of compilers and RTL, especially about JSON, RTTI, HTTP would end up in something as reinventing the mORMot. We just use what works.
Note that the generated client code does not depend at all from the other mORMot features, like ORM or SOA. It is perfectly uncoupled from those very powerful, but sometimes confusing features. With the client code, you will use the mORMot, without noticing it. The rodent will hide in its hole. But if you need it, e.g. for adding logs or services, it would be glad to help you. :-)
Enter the PetStore
The best known OpenAPI example is the famous "Pet Store" sample.
You can find a web preview of the whole API at https://petstore.swagger.io/
This API is defined in a JSON file, which is available in this gist.
Then we could write this small project:
program OpenApiPetStore; uses mormot.core.base, mormot.core.os, mormot.net.openapi; var p : TOpenApiParser; begin p := TOpenApiParser.Create('PetStore'); try p.Options := []; p.ParseFile(Executable.ProgramPath + 'petstore.swagger.json'); p.ExportToDirectory(Executable.ProgramPath); finally p.Free; end; end.
Note that a stand-alone command line tool is available, if you prefer, for the generation.
You could just execute in your shell (Windows or POSIX), to generate the same .pas files:
mopenapi petstore.swagger.json PetStore
With the default options, we get two units, one with the Data Transfer Objects (DTOs) and one with the actual client class.
You can see the result in this gist.
Here is just a simple method definition:
// getUserByName [get] /user/{username} // // Summary: Get user by user name // // Params: // - [path] Username (required): The name that needs to be fetched. Use user1 // for testing. // // Responses: // - 200 (main): successful operation // - 400: Invalid username supplied // - 404: User not found function GetUserByName(const Username: RawUtf8): TUser;
The TUser
record will be used as high-level result record for the method response. And you could define an option to generate plain string
values, if the mORMot RawUtf8=Utf8String
type is not what you need.
You can observe that the dto unit has only a few dependencies, so you could use it in your business logic code, without any "logic pollution" from the client unit.
The actual DTOs data structure are defined as records, so they don't need any create/free, and can be easily worked with. Some enumerates have been generated, from a list of string values, as specified in the original Petstore JSON definition. This makes your client code very readable, and error proof, because you won't be able to send any unattended value to the server.
The client "magic" is done in a wrapper class, named TPetStoreClient
, in the client unit.
Each method definition follows the expected specifications, and has very accurate comments generated from the description fields of the original JSON specification. If you find it too verbose, you can include the opoClientNoDescription
option. The methods are grouped per "tag", which is, in the OpenAPI jargon, a way to gather methods per subject. This is reflected in the code order, and also the comments.
If we define the following option (which matches the /concise
flag on mopenapi
command line):
p.Options := OPENAPI_CONCISE;
Then a single unit is generated, with almost no documentation within.
You can see this unit in this gist.
// store methods function GetInventory(): variant; function PlaceOrder(const Payload: TOrder): TOrder; function GetOrderById(OrderId: Int64): TOrder; procedure DeleteOrder(OrderId: Int64);
If the JSON specification had no actual layout for an answer, as in GetInventory()
above, we can't generate a DTO like TOrder
.
So we fallback to a variant, which could contain any JSON input after RTTI deserialization of the server response: a string, an integer, or more likely a complex object or array, encoded as a powerful mORMot TDocVariant
custom variant type. In the future, if you prefer, we may generate IDocList
and IDocDict
instances instead with a proper option - feedback is welcome.
As you may have noticed, the resulting code is very clean, especially if you compare with what the alternative solutions do actually generate. It is a good showcase of how to write mORMot code, with advanced features like cross-platform RTTI registration and JSON custom serialization.
More Complex APIs
During our tests and validation, we used some more complex API definitions.
For instance, we use internally an Ultravisor service, which single-file API code can be seen here.
It has a lot of DTOs and methods. Some enumerates have been generated and reused, when their actual elements do match in several places of the specifications. The resulting client unit is still very readable, even if we did not include all the available documentation in this OPENAPI_CONCISE
(for security reasons about publishing detailed information over a blog about an internal API, too).
You may observe that it also defined some Exception
classes, so that the generator is able to map the actual HTTP error codes (e.g. 401, 403...) to real pascal Exception
, with their own resultset DTO.
If an API is executed successfully, its client method just executes as expected, and returns its output values. Like regular local code.
But if the server returns an error code, then the client code will intercept it, map it to the designed Exception
class, and eventually raise it, with all its additional data in its Error
property:
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)); 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;
So you obtain a very natural way of handling API errors on the client side, with all the needed information in high-level pascal code, via standard try ... except on E: E#### do ...
blocks.
And the generator will properly document the mapping of HTTP error codes and Exception
classes, e.g. as in this following snippet of code:
// post_account_res_add_grant_auth [post] /accounts/{uuid}/add-grant-auth/ // // Summary: Gives a user permissions to grant authorization on the account // Description: // Roles: vm_admin for vm object, templates otherwise // // Params: // - [path] Uuid (required): Hypervisor uuid // - [body] Payload (required) // // Responses: // - 200 (main): Success // - 400 [EValidationErrorResponse]: Parameters have invalid format or type // - 401 [EUnauthorizedResponse]: User is not authenticated // - 403 [EForbiddenResponse]: User has insufficient permissions // - 404 [EResourceNotFoundError]: Hypervisor not found // - 422 [EIntegrityErrorResponse]: Parameters have valid format but are not compatible // with the server state function PostAccountResAddGrantAuth(const Uuid: RawUtf8; const Payload: TUserShort): TDbAccount;
Another example of an API comes from Paolo Rossi reference material.
You can find in this gist its JSON specifications, its dto and client units, and its single api unit.
Here is an extract of one generated method, and its implementation:
// sign_delete [delete] /scope/{job} // // Description: // delete a verification job // // Params: // - [path] Job (required): Job ID (20 chars) // // Responses: // - 200 (main): Successfully deleted // - 404 [EError]: Job not found `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); end;
A nice example of the rendering of our code generator, which leverages the mORMot core features. And this code will work on the very old Delphi 7!
Feedback is Welcome
Of course, we did not fully implement all OpenAPI v3.1 specifications. So if you have any concern with this generator, feel free to report your problematic JSON on our forum, and we would try to make the proper arrangements.