The upcoming kbmMW update which were supposed to be a minor bug-fix update, will however also contain a new major feature. A client stub generator framework.
What is a client stub generator framework?
It is a framework which, on the basis of smart services, can generate code that can be used directly by clients of various types, to access the kbmMW based application servers HTTP smart services.
Currently kbmMW already contains a smart client feature, which makes it very easy to write native kbmMW clients that can access smart service based features. Since the smart client relies on late binding, the developer however do not get much IDE and compiler assistance regarding arguments and their types. For that to happen, the compiler will need some generated code that explains the exposed server side functions and methods for the compiler and the IDE.
This is where a stub generator comes into play. This article will however not talk about how to generate Delphi client side stubs for smart services, as that particular feature will probably not make it into the upcoming update, although preparations for it is already in the code.
Instead I have focused on utilizing the stub generator framework to implement another even more complex set of code, that fulfills a typical requirement, found in the REST producing world. I particularly recognized it well over a year ago, when I was consulting large companies doing Java code.
In the Java world, it is a defacto standard to document REST interfaces using something that, at the time, was called Swagger. Later it has been renamed to OpenAPI, which is now recognized and supported by the absolute majority of big players supporting REST.
OpenAPI provides a description of your REST interface, which can be used both for documentation, but also for automatically generating (stub) code for all sorts of development environments, to make it easy for those environments to utilize the features published via the REST interface.
You can read much more about OpenAPI here: https://swagger.io/
The OpenAPI initiative has not only produced a defacto standard but also various tools one can use to generate, edit, view and test REST interface descriptions (in common terms called Swagger files).
One such tool is the Swagger-UI (user interface) which consists of Javascript and HTML which can be served by a web server to provide a simple to use user interface to the REST interfaces that has been exposed in the server.
kbmMW now supports all this fully.
Let’s simply start with showing how the REST interface of the kbmMW SimpleInvocation demo server looks like, using Swagger-UI:
On the left, one can see an OpenAPI declaration of the exposed REST interfaces, and on the right one can see a user friendly interface where those interfaces can be called by simple button clicks. It is even easy to fill arguments and such that the REST method requires.
If I scroll the right side down to the AddNumbers method, and click the bar, it opens up with additional information, and a button that let us try the REST call.
This is imo pretty cool 🙂
So how do we OpenAPI enable a REST capable application server?
It is really easy.
To return the OpenAPI specification of the REST services, we simply add another REST exposed method to the service in Unit2.
[kbmMW_Rest('method:get, path: "api", responseMimeType:"application/x-yaml"')] function OpenAPI:string;
We can named the method and its REST path anything you like, but we should provide a correct responseMimeType. The standard for OpenAPI descriptions is to be expressed in YAML, which kbmMW fortunately fully supports. It is also allowed to produce OpenAPI descriptions in JSON, but it is more a way to be compatible with systems that do not support YAML. So in this example, we specify in the mimetype that the response will be YAML.
// Return OpenAPI specification. function TkbmMWCustomService2.OpenAPI:string; begin // Return OpenAPI specification for all REST methods in this service // as YAML. Add the ASettings value: 'json:true' to return the specification // as JSON. // Add 'servers: [ "url1", "url2",.. "urln" ]' to ASettings if you want to // embed server location information in the specification. // Add 'inline:true' to inline object definitions instead of using $ref. // The example in the next line utilize the configuration framework to make // the setting easily configurable. Result:=TkbmMWSmartOpenAPIStubGenerator.GenerateOpenAPI('',self,'inline:$(OpenAPI.inline=false)'); end;
The code in the OpenAPI function is pretty simple. It just calls the GenerateOpenAPI method of the OpenAPI stub generator with the service to be “OpenAPI’ified” and optionally a settings string. The settings string can be empty
In this sample, the settings string contains the value
inline:$(OpenAPI.inline=false)
The reason is that there are two valid ways to generate OpenAPI specifications for REST interfaces that takes or returns objects, inline or by reference.
Inline means that each object is explained in detail every place it can be used in all descriptions for the REST calls, while by reference means that OpenAPI style components (objects) are described and referenced where needed. By reference is the default. However in the sample code, I have chosen to let it be configurable with the help of the kbmMW configuration framework. We could have written: inline:false or inline:true, but the sample instead asks the configuration for the current value of the OpenAPI.inline value that can be either true or false. If no such value is found, kbmMW will use the default value of false (as indicated after the =).
One could also have specified that JSON is preferred. That just requires adding json:true to the settings string like this:
inline:$(OpenAPI.inline=false), json:true
That could obviously also have been made configurable the same way as inline.
However we leave it as is, so the output of the OpenAPI function will be a YAML formatted OpenAPI description of our REST methods in the service.
Hence if we use a browser to open the URL: //localhost:888/myserver/api we will get the complete OpenAPI description:
openapi: "3.0.0" info: title: SMARTDEMO description: "HTTP smart service supp. FastCGI" version: "1" paths: /myserver/api: get: operationId: get_myserver_api responses: "200": content: application/x-yaml: schema: type: string description: "Success response" /myserver/helloworld: get: operationId: get_myserver_helloworld responses: "200": content: text/plain: schema: type: string description: "Success response" /myserver/now1: get: operationId: get_myserver_now1 responses: "200": content: text/plain: schema: type: string format: date-time description: "Success response" /myserver/now2: get: operationId: get_myserver_now2 responses: "200": content: text/plain: schema: type: number format: double description: "Success response" /myserver/echostring/{AString}: get: operationId: get_myserver_echostring__AString_ parameters: - in: path name: AString required: true schema: type: string responses: "200": content: text/plain: schema: type: string description: "Success response" /myserver/myechostring/{AString}: get: operationId: get_myserver_myechostring__AString_ parameters: - in: path name: AString required: true schema: type: string responses: "200": content: text/plain: schema: type: string description: "Success response" /myserver/echourl: get: operationId: get_myserver_echourl responses: "200": content: text/plain: schema: type: string description: "Success response" /myserver/echoheader: get: operationId: get_myserver_echoheader parameters: - in: header name: Accept required: true schema: type: string responses: "200": content: text/plain: schema: type: string description: "Success response" /myserver/echoanyheader/{AHeaderName}: get: operationId: get_myserver_echoanyheader__AHeaderName_ parameters: - in: path name: AHeaderName required: true schema: type: string responses: "200": content: text/plain: schema: type: string description: "Success response" /myserver/echocookie: get: operationId: get_myserver_echocookie parameters: - in: cookie name: MyCookie required: true schema: type: string responses: "200": content: text/plain: schema: type: string description: "Success response" /myserver/echoreversedstring: post: operationId: post_myserver_echoreversedstring requestBody: required: true content: text/plain: schema: type: string responses: "200": content: text/plain: schema: type: string description: "Success response" /myserver/echobytes: post: operationId: post_myserver_echobytes requestBody: required: true content: text/plain: schema: type: string format: byte responses: "200": content: text/plain: schema: type: string format: byte description: "Success response" /myserver/echoreversedconfigstring: get: operationId: get_myserver_echoreversedconfigstring responses: "200": content: text/plain: schema: type: string description: "Success response" /someabspath/addnumbers: get: summary: "Adds two numbers and returns result" operationId: add_numbers parameters: - in: query name: arg1 required: true description: "First numeric argument" schema: type: integer format: int32 - in: query name: arg2 required: true description: "Second numeric argument" schema: type: integer format: int32 responses: "200": content: text/plain: schema: type: integer format: int32 description: "The result of the added numbers" /myserver/storeperson: post: operationId: post_myserver_storeperson requestBody: required: true content: text/plain: schema: "$ref": "#/components/schemas/person" responses: "200": content: text/plain: schema: type: integer format: int32 description: "Success response" /myserver/getperson/{id}: get: operationId: get_myserver_getperson__id_ parameters: - in: path name: id required: true schema: type: integer format: int32 responses: "200": content: application/json: schema: "$ref": "#/components/schemas/person" description: "Success response" /myserver/getpersons: get: operationId: get_myserver_getpersons responses: "200": content: application/json: schema: type: array items: "$ref": "#/components/schemas/person" description: "Success response" components: schemas: person: properties: Name: type: string Address: type: string Age: type: integer format: int32 type: object title: person
Feel free to study it if you want to. You may notice that several places, there are facilities to add summaries and descriptions. For example check the addnumbers call:
/someabspath/addnumbers: get: summary: "Adds two numbers and returns result" operationId: add_numbers parameters: - in: query name: arg1 required: true description: "First numeric argument" schema: type: integer format: int32 - in: query name: arg2 required: true description: "Second numeric argument" schema: type: integer format: int32 responses: "200": content: text/plain: schema: type: integer format: int32 description: "The result of the added numbers"
Where does the summary and descriptions come from?
Looking at the definition of the AddNumbers function in Unit2.pas, it becomes obvious:
// Add two numbers. // It can be called from regular clients, smart clients // and REST clients. // It can be called from a browser like this: // http://.../someabspath/addnumbers?arg1=10&arg2=20 [kbmMW_Method] [kbmMW_Rest('method:get, path: "/someabspath/addnumbers", '+ 'id:"add_numbers", '+ 'summary:"Adds two numbers and returns result", '+ 'resultDescription:"The result of the added numbers"')] function AddNumbers([kbmMW_Rest('value: "$arg1", required: true, description:"First numeric argument"')] const AValue1:integer; [kbmMW_Rest('value: "$arg2", required: true, description:"Second numeric argument"')] const AValue2:integer; [kbmMW_Arg(mwatRemoteLocation)] const ARemoteLocation:string):integer;
There is even an id value in the kbmMW_Rest attribute. OpenAPI requires that each REST path must have a unique ID. kbmMW will automatically attempt to generate one, but you can choose your own ID names via the id syntax as seen above. id, summary, description and resultDescription are (in kbmMW) all optional. If OpenAPI require a descriptive value and none has been given, kbmMW provides a default.
So now we can generate valid OpenAPI descriptions. How do we get around to letting our kbmMW based application server present them using Swagger-UI
Because kbmMW can act as a web server, this is actually also pretty easy. We simply add a TkbmMWFilePool instance to the main form (Unit1) and set the FilePool property of the service datamodule (Unit2) to point to it.
Now kbmMW will work as a regular web server, and attempt to serve a file when it does not find a REST function to be called.
In the same directory as the SimpleInvocation server executable, you should create a directory called MyServer (to match the service) and under it, we add an api directory, both just to match the logical path of the function OpenAPI in Unit2. In fact you do not have to use this specific path hierarchy but I have chosen so for the demo.
In the api directory we will put files downloaded from https://swagger.io/tools/swagger-ui/
Then
And
You can either download all the files from the dist folder, one by one to the api\dist directory, or you can download everything via the Clone or download button. If you do the later, then open the downloaded zip file and extract the dist folder with contents to the api directory.
Finally add a file called index.html to the api directory. You can copy/paste its contents from this:
<!DOCTYPE html> <!-- HTML for static distribution bundle build --> <html lang="en"> <head> <meta charset="UTF-8"> <title>Swagger Editor</title> <style> * { box-sizing: border-box; } body { font-family: Roboto,sans-serif; font-size: 9px; line-height: 1.42857143; color: #444; margin: 0px; } #swagger-editor { font-size: 1.3em; } .container { height: 100%; max-width: 880px; margin-left: auto; margin-right: auto; } #editor-wrapper { height: 100%; border:1em solid #000; border:none; } .Pane2 { overflow-y: scroll; } </style> <link href="./dist/swagger-editor.css" rel="stylesheet"> <link rel="icon" type="image/png" href="./dist/favicon-32x32.png" sizes="32x32" /> <link rel="icon" type="image/png" href="./dist/favicon-16x16.png" sizes="16x16" /> </head> <body> <div id="swagger-editor"></div> <script src="./dist/swagger-editor-bundle.js"> </script> <script src="./dist/swagger-editor-standalone-preset.js"> </script> <script> window.onload = function() { // Build a system const editor = SwaggerEditorBundle({ dom_id: '#swagger-editor', layout: 'StandaloneLayout', presets: [ SwaggerEditorStandalonePreset ] }) window.editor = editor } </script> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position:absolute;width:0;height:0"> <defs> <symbol viewBox="0 0 20 20" id="unlocked"> <path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V6h2v-.801C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8z"></path> </symbol> <symbol viewBox="0 0 20 20" id="locked"> <path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8zM12 8H8V5.199C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8z"/> </symbol> <symbol viewBox="0 0 20 20" id="close"> <path d="M14.348 14.849c-.469.469-1.229.469-1.697 0L10 11.819l-2.651 3.029c-.469.469-1.229.469-1.697 0-.469-.469-.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-.469-.469-.469-1.228 0-1.697.469-.469 1.228-.469 1.697 0L10 8.183l2.651-3.031c.469-.469 1.228-.469 1.697 0 .469.469.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c.469.469.469 1.229 0 1.698z"/> </symbol> <symbol viewBox="0 0 20 20" id="large-arrow"> <path d="M13.25 10L6.109 2.58c-.268-.27-.268-.707 0-.979.268-.27.701-.27.969 0l7.83 7.908c.268.271.268.709 0 .979l-7.83 7.908c-.268.271-.701.27-.969 0-.268-.269-.268-.707 0-.979L13.25 10z"/> </symbol> <symbol viewBox="0 0 20 20" id="large-arrow-down"> <path d="M17.418 6.109c.272-.268.709-.268.979 0s.271.701 0 .969l-7.908 7.83c-.27.268-.707.268-.979 0l-7.908-7.83c-.27-.268-.27-.701 0-.969.271-.268.709-.268.979 0L10 13.25l7.418-7.141z"/> </symbol> <symbol viewBox="0 0 24 24" id="jump-to"> <path d="M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.41L5.83 13H21V7z"/> </symbol> <symbol viewBox="0 0 24 24" id="expand"> <path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/> </symbol> </defs> </svg> </body> </html>
Now you are ready to rock and roll.
Start the server. Then start a browser and type:
http://localhost:888/myserver/api/index.html?url=/myserver/api
This instructs kbmMW to serve the index.html file, which in turn requests remaining files needed for producing the Swagger-UI interface. Finally we tell Swagger-UI to load the OpenAPI description from the /myserver/api URL (which, if you remember, will call the function OpenAPI in Unit2).
You should now end up with a view similar to what was shown in start of this blog post.
From within the Swagger-UI interface, you can generate both server skeletons and client stubs for all the REST features exposed by kbmMW.
Happy Swagging 😉
If you like kbmMW, share the word. Reshare the blog posts and let others know about the product!
Essentially help us to help you 🙂
The code in the OpenAPI function is pretty simple. It just calls the GenerateOpenAPI method of the OpenAPI stub generator with the service to be “OpenAPI’ified” and optionally a settings string. The settings string can be empty
with the service to be “OpenAPI’ified” ??
Yes
Hi,
For the life of me i can’t get this to work. I can get the api as a download file buthttp://localhost:888/myserver/api/index.html?url=/myserver/api
gives me back a “Request not supported”. I followed the instructions as best as possible.
5.10.20 Community Edition
on Delphi 10.3 CE
Thanks.