Blog

All Blog Posts  |  Next Post  |  Previous Post

Diving deeper: Cloudbase REST in Delphi, Part 1/3: Basics

Bookmarks: 

Thursday, September 22, 2022

TMS Software Delphi  Components

Intro

TMS FNC Core is a universal core layer for creating rich visual and non-visual components for VCL, FMX, LCL and WEB core apps. A major part of TMS FNC Core is the ability to create and execute REST requests.

The "Diving Deeper: Cloudbase REST" blog series will consist out of three parts:

  1. Basics, getting to know Cloudbase REST
  2. Extended, various special REST specific requirements and how to implement them 
  3. Sync vs Async operations

The blog series will use a test REST service. Note that when referring to unit names, the prefix of the framework ((FMX.)(VCL.)(WEBLib.)(LCL)) will not be included, for readability purposes. The code snippets are written in FMX as a default framework, but can easily be ported to other frameworks. If you have any questions during the blog series, don't hesitate to ask them in the comments section, or by using our support channels.

Units

After installing TMS FNC Core, the most important units are the TMSFNCCloudBase.pas and the TMSFNCUtils.pas unit.

  • TMSFNCCloudBase.pas: Unit containing interfaces and functions to create and execute requests, and to capture and parse the result.
  • TMSFNCUtils.pas: Unit containing a lot of class helper functions which, in some situations, are helpful to build the request with proper encoding, or to easily convert data to a specific type required for the REST request to be executed. Additionally, this unit contains class wrappers to parse JSON, which in many situations is the result string from the request.

Getting Started

We get started by adding the unit TMSFNCCloudBase. There is a class called TTMSFNCCloudBase, that provides the required functions and properties to setup a REST Request. To create an instance of TTMSFNCCloudBase, call

c := TTMSFNCCloudBase.Create;
To access and set the request data before sending it, use the following properties. Note that the data is not real, it's to show the capabilities of the request. The next chapter sample code will include a real service with real data.

c.Request.Clear;
c.Request.Host := 'https://myhost.com';
c.Request.Method := rmGET;
c.Request.Path := '/mypath/upload';
c.Request.Query := 'param1=value&param2=value';
c.Request.PostData := 'MyPostData';
c.Request.ResultType := rrtString; //rrtStream //rrtFile
c.Request.AddHeader('Header1', 'Header Data');
To execute a request call

c.ExecuteRequest(
  procedure(const ARequestResult: TTMSFNCCloudBaseRequestResult)
  begin
    //Parse ARequestResult
  end
  );
Note that the request result is captured in an anonymous callback, which is asynchronous by default. The last chapter will cover this and show the difference between synchronous and asynchronous requests. Note that the above code is going to generate a memory leak. Always make sure to destroy the instance of TTMSFNCCloudBase at some point in your application. The above calls are not taking this into account. The best practice is to create an instance when the application is starting, and destroy it when the application is closing. This way, you can create and manage multiple requests with one instance during the lifetime of your application.

After executing the request, the callback is triggered. The ARequestResult of type TTMSFNCCloudBaseRequestResult will contain the result of the request. There are 3 types of result request content. The request result type needs to be set before executing the request.

  • rrtString: The default return value of the request result. Can be XML, JSON, or any other type of text. The property ARequestResult.ResultString contains the value.
  • rrtStream: Returns the content as a stream, the property ARequestResult.ResultStream contains the content
  • rrtFile: Immediately saves the content to a file, specified by c.Request.ResultFile before executing the request.

Supported Resource Methods

Below is a list of supported resource methods in TTMSFNCCloudBase

  • rmGET: The GET method is used to read/retrieve the content of a resource.
  • rmPOST: The POST method is most-often utilized to create new resources.
  • rmPOSTMULTIPARTRELATED, 
  • rmPOSTMULTIPART: Same as POST, but with a special multi-part post data body for sending special request content such as images, files or any other binary content.
  • rmPUT: PUT is most-often utilized for updating resources.
  • rmPUTMULTIPART, 
  • rmPUTMULTIPARTRELATED: Same as PUT, but with a special multi-part post data, similar to POST multipart.
  • rmDELETE: The DELETE method is used to delete a resource.
  • rmPATCH: The PATCH method is used to modify a resource, mostly an incremental difference between the original resource and the new resource.
  • rmUPDATE: An equivalent for the rmPUT resource method.

For testing purposes we use httpbin.org which is a service to test REST requests. The first part of this blog post is not going to cover all of the above resource methods. In this blog post we are going to cover the GET and POST methods and the next blog post will go a bit deeper and will cover a couple of special cases.

GET

To execute a GET request we use the following code.

c.Request.Clear;
c.Request.Host := 'https://httpbin.org';
c.Request.Method := rmGET;
c.Request.Path := '/get';
c.Request.ResultType := rrtString;
c.ExecuteRequest(
  procedure(const ARequestResult: TTMSFNCCloudBaseRequestResult)
  begin
    if ARequestResult.Success then
      Memo1.Text := ARequestResult.ResultString
    else
      Memo1.Text := 'Request failed';
  end
  );
To check if the result is actually successfully returned from the service, we can check the ARequestResult.Success boolean which actually internally maps on ARequestResult.ResponseCode and verifies if the code is a valid HTTP response code. To learn more about the various response codes, visit https://developer.mozilla.org/en-US/docs/Web/HTTP/Status. In most cases, the ARequestResult.ResultString will also contain a reason why the request failed, paired with a response code. This can typically be one of the following reasons:

  • Credentials are missing such as API key, client-id & secret which are required for services that require authentication
  • URL is malformed
  • Data is not correctly formatted or is missing required parameters
  • ...

When executing the request, and the request succeeds, we get back JSON. the JSON contains information about the request.

{
  "args": {}, 
  "headers": {
    "Cache-Control": "no-cache", 
    "Host": "httpbin.org", 
    "User-Agent": "Mozilla/5.001 (windows; U; NT4.0; en-US; rv:1.0) Gecko/25250101", 
    "X-Amzn-Trace-Id": "Root=1-63246a5a-341eb67801e5c0f00897996f"
  }, 
  "origin": "X", 
  "url": "https://httpbin.org/get"
}

For example, if we would change the request to include query parameters like the code below:

c.Request.Clear;
c.Request.Host := 'https://httpbin.org';
c.Request.Method := rmGET;
c.Request.Path := '/get';
c.Request.Query := 'param1=Hello%20World&param2=My%20First%20Request';
c.Request.ResultType := rrtString;
c.ExecuteRequest(
  procedure(const ARequestResult: TTMSFNCCloudBaseRequestResult)
  begin
    if ARequestResult.Success then
      Memo1.Text := ARequestResult.ResultString
    else
      Memo1.Text := 'Request failed';
  end
  );
The response is

{
  "args": {
    "param1": "Hello World", 
    "param2": "My First Request"
  }, 
  "headers": {
    "Cache-Control": "no-cache", 
    "Host": "httpbin.org", 
    "User-Agent": "Mozilla/5.001 (windows; U; NT4.0; en-US; rv:1.0) Gecko/25250101", 
    "X-Amzn-Trace-Id": "Root=1-63246d6a-00d0a592795a4fcd73753f6d"
  }, 
  "origin": "X", 
  "url": "https://httpbin.org/get?param1=Hello World&param2=My First Request"
}
as you can see, the parameters that are passed as a query parameter to the URL are retrieved, parsed and used as parameters for the request. Note that we have to properly encode the URL. In TMSFNCUtils, there is a utility function available to that so you won't have to worry about proper encoding. To apply this to our request query property we change

c.Request.Query := 'param1=Hello%20World&param2=My%20First%20Request';

to

c.Request.Query := 'param1=' + TTMSFNCUtils.URLEncode('Hello World') + '&param2=' + TTMSFNCUtils.URLEncode('My First Request');

POST

As explained a POST request is sending content to the service. For httpbin.org we can send any content we want, but other services typically require specific data information and in a particular format. To send data to the service we use the PostData property. Below is an example

c.Request.Clear;
c.Request.Host := 'https://httpbin.org';
c.Request.Method := rmPOST;
c.Request.Path := '/post';
c.Request.PostData := '{"MyData":"Hello World"}';
c.Request.ResultType := rrtString;
c.ExecuteRequest(
  procedure(const ARequestResult: TTMSFNCCloudBaseRequestResult)
  begin
    if ARequestResult.Success then
      Memo1.Text := ARequestResult.ResultString
    else
      Memo1.Text := 'Request failed';
  end
  );
The result we get back is

{
  "args": {}, 
  "data": "{\"MyData\":\"Hello World\"}", 
  "files": {}, 
  "form": {}, 
  "headers": {
    "Cache-Control": "no-cache", 
    "Content-Length": "24", 
    "Host": "httpbin.org", 
    "User-Agent": "Mozilla/5.001 (windows; U; NT4.0; en-US; rv:1.0) Gecko/25250101", 
    "X-Amzn-Trace-Id": "Root=1-632473d8-5d71c7d1106087a277a7713b"
  }, 
  "json": {
    "MyData": "Hello World"
  }, 
  "origin": "X", 
  "url": "https://httpbin.org/post"
}
We clearly see that the JSON data we sent is accepted by the service and the response shows the received data.

Helper Methods

In the cloud base unit there are a couple of helper methods/functions available to easily setup a rest request. This technique can be used to download a file, or request data with a simple request to an URL. Below is a list of methods or functions that can be used to achieve this.

function HTTPPostDataBuilder: TTMSFNCCloudBaseRequestPostDataBuilder;
procedure HTTPClearHeaders;
procedure HTTPAddHeader(const AName: string; const AValue: string);
procedure HTTPCloudRequest(const AURL: string; AResultType: TTMSFNCCloudBaseRequestResultType = rrtString; AMethod: TTMSFNCCloudBaseRequestMethod = rmGET; const ARequestResultEvent: TTMSFNCCloudBaseRequestResultEvent = nil); overload;
procedure HTTPCloudRequest(const AHost, APath, AQuery, APostData: string; AResultType: TTMSFNCCloudBaseRequestResultType = rrtString; AMethod: TTMSFNCCloudBaseRequestMethod = rmPOST; const ARequestResultEvent: TTMSFNCCloudBaseRequestResultEvent = nil); overload;
With the above methods, you can basically prepare the headers and postdata and then call the request in one line. Internally, the HTTPCloudRequest method will asynchronously build and execute the request and trigger the callback when finished.

TTMSFNCCloudBase.DownloadFileFromURL('https://www.myserver.com/myfile.zip',
procedure(const ARequestResult: TTMSFNCCloudBaseRequestResult)
begin
  ARequestResult.ResultStream.SaveToFile('myfile.zip');
end);

The class method DownloadFileFromURL will download the file specified as a parameter to a memory stream (ARequestResult.ResultStream) and then allow to save it to a file.

Feedback

Next up will be a more extended guide around REST and TTMSFNCCloudBase, so stay tuned for more to come! As always, please leave a comment or if you have any questions, don't hesitate to ask us!



Pieter Scheldeman


Bookmarks: 

This blog post has received 4 comments.


1. Thursday, October 6, 2022 at 12:00:36 AM

This API seems... well... not very elegant. Having to add all the parameters yourself in "query"? Splitting host and path? Objects, objects everywhere? An execute method that has an anonymous function embedded in it?

I know Delphi is a terrible language to design elegant APIs in since it lacks many modern features, but couldn''t you have studied the best REST libraries available across many languages and tried to model after one or several of them?

Again, I know the problem here is more Delphi than TMS, but your first httpbin get example would look like this in Python using the Requests library:

import requests

c = requests.get("https://httpbin.org/get")
if c.status_code == requests.codes.success:
print(c.text)
else:
print("Request failed")

Folks, no human being is ever going to, NOR SHOULD THEY EVER HAVE TO, remember "TTMSFNCCloudBaseRequestResult". That''s positively Java-ish. The native Delphi request library is even worse, requiring users to remember "TRESTRequestParameterKind.pkGETorPOST".

The Requests library does it right. a function for each method. Delphi libraries all seem to create one mega-object that needs creating, executing, destroying.

In your example you need to create an object, clear the object for some reason, set the method, set the result type for some baffling reason (shouldn''t that be determined by what actually comes back?), call an execute method and for some reason I can''t fathom write a result handler INSIDE the execute method which includes memorizing "TTMSFNCCloudBaseRequestResult". Why the heck not just put the status code inside the object and let the user check it?

Another head-scratcher:

c.Request.Query := ''param1='' + TTMSFNCUtils.URLEncode(''Hello World'') + ''¶m2='' + TTMSFNCUtils.URLEncode(''My First Request'');

None of you see how verbose, bloated and ugly that is?
Why not just AUTOMATICALLY encode the URL? Again, the following example is nicer than it ever could be in Delphi because Python has dictionary literals, but the Requests library would handle the parameters like this:

requests.get("https://httpbin.org/get", params={"param1": "Hello world", "param2": "My first request"})

Note no need to build the URL yourself, no need to manually encode/escape anything or build a string studded with "TTMSFNCUtils.URLEncode".

You folks make a lot of nice software, but the interface being offered for this REST library is rather weird. Even with Delphi''s limitations in mind a lot of the design doesn''t make sense (the anonymous function in particular).

I can''t imagine choosing to do a lot of REST work with this library (or really any Delphi REST library). So much manual work that could/should be handled by libraries, especially when it''s a commercial library.


Joseph


2. Friday, October 7, 2022 at 5:33:39 PM

Despite what Joseph said, I find this library to be very straight-forward and easy to use/understand.

As for his encoding argument, again I think this library handles it very well. I have had to interact with several REST services and there are instances where the encoding varies.

Delphi does in fact have many modern features and, in my opinion, the TMS FNC CloudBase library is very well designed and takes advantage of some of those features.

But hey, to each their own. I personally love the way Delphi does things, the verbosity makes the code self documenting and that is a form of elegance itself. And TMS makes excellent libraries that are just plain easy to use.

Matthew Vesperman


3. Friday, October 7, 2022 at 8:30:32 PM

Many thanks for your comments Matthew!

Bruno Fierens


4. Friday, October 7, 2022 at 9:28:03 PM

Joseph, TTMSFNCCloudBase is designed initially as a base for TMS FNC Cloud Pack. It’s an abstract class and needs to deal with a lot of special cases. These blog series are written to provide code snippets building your own service wrapper that’s not already covered. This is basically “free”, because TMS FNC Core is included in any FNC component set. The idea behind this was actually, why not open it to the community instead of hiding it in plain sight.

Aside from that, there are easy convenient functions / class helpers that haven’t been mentioned here like the

SimpleGETAsString(‘https://httpbin.org/get’);

Which is even easier than the Python implementation. You can even choose to run it synchronous or asynchronous.

The TTMSFNCCloudBaseRequestResult is an object containing all information you need from the request and the response: headers, status code, result stream or string. No need to search for anything else.

I would say, give it a chance. It handles easy things and can also handle more complex cases.

Pieter Scheldeman




Add a new comment

You will receive a confirmation mail with a link to validate your comment, please use a valid email address.
All fields are required.



All Blog Posts  |  Next Post  |  Previous Post