Besides a bunch of fixes, Smart 2.1.2 introduced a new unit, SmartCL.Inet.REST. This unit greatly simplifies writing REST clients in Smart. A simplest way to introduce it is with an example.
Introduction
Let’s say we have a REST server listening at http://someserver/hbbtv/streamevents. It supports one request – PUT – and three parameters – applicationid, eventid, and eventdata. To send a request, one would just open a HTTP connection (TW3HttpRequest would do that for you in Smart) and send something like http://someserver/hbbtv/streamevents/applicationid=1&eventid=233&eventdata=Test (or http://someserver/hbbtv/streamevents/1/233/Test if your REST server is not completely braindead). Still, you would have to set up the TW3HttpRequest object, concatenate the URL and parse the response. And whenever you write some code, there’s an opportunity to introduce a bug so it’s best if you code as little as possible.
The REST client for the example above can be written in one statement.
REST['someserver', '/hbbtv/streamevents'] .Get([1, 233, 'Test']) .OnDone(lambda (http) Log('Event sent'); end) .OnError(LogHttpError);
We can deconstruct the code as follows:
- REST[server, path] specifies an API – an entry point to a REST server. Typically you would store the result of this operation in a local variable or in a field if you issue more than one request to the same API.
- .Get([parameters]) constructs an URL request and sends it to the server.
- .OnDone provides an event which is called on completion (when response is received from the server).
- .OnError provides an event which is called in case of a TCP error (server not found etc). This event is also called if REST server respondes with a HTTP code not in the 2xx range (for example with 404 not found).
The improvements over plain TW3HttpRequest become even more obvious in a real-life application. The code fragment below comes from a small Smart app which I’m using for in-house tests. It allows the user to enter server name and all three parameters and then sends the request when a button is clicked.
const CHbbTvAPI = '/hbbtv/streamevents'; REST[inpServerAddress.Text, CHbbTvAPI] .Get([inpApplicationID.Text, inpEventID.Text, inpEventData.Text]) .OnDone(lambda (http) Log('Event sent'); end) .OnError(LogHttpError);
More examples
Let’s take a look at few other examples. New in Smart 2.1.2 is the REST Client demo, which can be found in the Demos\Featured Demos\Business\ folder. It contains many examples which access public REST servers kindly provided by http://jsontest.com.
The IP Address button demonstrates the simplest possible REST call.
// simple inline create&send request // Create: REST[] // Send: Get() REST['http://ip.jsontest.com', ''] .Get() .OnDone(LogIPAddress) .OnError(LogRESTError);
The path is empty (which is equivalent to the ‘/’ path) and there are no parameters. The server, however, sends a result, which is processed in the LogIPAddress method.
procedure TFormMain.LogIPAddress(http: TW3HttpRequest); begin // simple response processor var resp := JSON.Parse(http.ResponseText); // resp is of type Variant if VarIsValidRef(resp.ip) then Log('Your IP address is ' + resp.ip) else Log('Failed to parse server response: ' + http.ResponseText); end;
The error handler LogHttpError is used in all examples and simply logs HTTP status code and response text.
procedure TFormMain.LogRESTError(http: TW3HttpRequest); begin Log('ERROR ' + http.Status.ToString + ' ' + http.StatusText); Log(' ' + http.ResponseText); end;
The Date and time example is very similar except that the REST API is initialized in the InitializeForm method. Button’s OnClick handler then just issues the Get request and handles the response.
// create 'Date & time' API entry point FDateAPI := REST['http://date.jsontest.com', '']; // set default error handler FDateAPI.OnError(LogRESTError); procedure TFormMain.btnDateTimeClick(Sender: TObject); begin // use pre-created API FDateAPI.Get().OnDone(LogDateTime); end;
LogDateTime shows a different approach to JSON parsing. Instead of Variant, we can define an appropriate class derived from JObject (in this example TRestData which itself derives from JObject is used as a parent).
//Expected JSON response: //{ // "time": "12:10:22 PM", // "milliseconds_since_epoch": 1418559022932, // "date": "12-14-2014" //}
type TDateAndTime = class(TRestData) time: string; date: string; end; procedure TFormMain.LogDateTime(http: TW3HttpRequest); begin // use an object to accept returned data var dt := TDateAndTime(JSON.Parse(http.ResponseText)); // dt.date will be 'undefined' if a bad response is returned // this may cause problems in a more complex code but works fine here Log('Date: ' + dt.date); // better way is to test explicitely Log('Time: ' + if VarIsValidRef(dt.time) then dt.time else 'unknown'); end;
The third test, Echo, is a bit weird, as the server expects URL in form of /key/value pairs like http://echo.jsontest.com/one/test/two/test2/three/test3. This example URL will return the following JSON response.
{ "two": "test2", "one": "test", "three": "test3" }
The code uses hardcoded key names one and two and just inserts user-provided values in appropriate places. Response is not parsed, just logged.
// use a on-the-flight created API REST['http://echo.jsontest.com', ''] // append parameters to the path .Get(['one', inpEcho1.Text, 'two', inpEcho2.Text]) // just log the response without parsing; anonymous method is used this time .OnDone(lambda(http) Log('Response: ' + http.ResponseText); end) // standard error handler .OnError(LogRESTError);
The validate.jsontest.com service validates a given JSON object against the reference parser. Entry point is created in the InitializeForm method.
// create 'Validation' API entry point and set default error handler FValidationAPI := REST['http://validate.jsontest.com', '/'].OnError(LogRESTERror);
There are two buttons on the form – one sends a valid JSON data and the other an invalid one.
Valid JSON is created by stringifying an instance of the TDateAndTime object. Data is then sent to the server with the Post method (instead of a Get as in all examples above). Because the server expects the JSON string to be passed in a HTML encoded form, we must encode it with HTMLTextEncode. We must also set Content-Type header to be application/x-www-form-urlencoded. To do that, the code passes an additional parameter header: array of string; to the request.
// send a valid JSON and log the response // test JSCN is built directly from a JavaScript object and should therefore always be valid var testJson := JSON.Stringify(TDateAndTime.Create); // validate.jsontest.com expects parameter to be in a form of a key=value pair // also, content type must be set to x-www-form-urlencoded so we'll send an appropriate header FValidationAPI.Post('json=' + HTMLTextEncoder.Encode(testJson), [], ['Content-Type: application/x-www-form-urlencoded']) .OnDone(lambda(http) Log('Response: ' + http.ResponseText); end);
The second button sends a manually created invalid JSON. This time, the JSON is sent as a Get request parameter which simplifies the call as we don’t have to provide the custom content type. The server expects a named parameter (json=…) and currently the only way to achieve this is to manually prefix the parameter with the ?json= string.
// send an invalid JSON and log the response // this time, JSON is sent with Get, not Post FValidationAPI.Get(['?json=' + HTMLTextEncoder.Encode('{"key":"value"')]) .OnDone(lambda(http) Log('Response: ' + http.ResponseText); end);
The last example sends a string to the server and receives a JSON response containing MD5 hash of the string and original data. Two equivalent ways to call the server – using a Get and a Post are shown.
type TMD5Response = class(TRestData) md5: string; original: string; end; procedure TFormMain.btnMD5Click(Sender: TObject); begin var FMD5API := REST['http://md5.jsontest.com', '']; var source := HTMLTextEncoder.Encode(inpMD5Source.Text); // you can also set an onPrepare handler which can inspect/modify // http request before it is sent FMD5API.Get(['?text=' + source], [], LogHttpDetails).OnDone(LogMD5); // we can also modify headers in onPrepare instead of passing them // as a parameter FMD5API.Post('text=' + source, [], [], LogHttpDetails).OnDone(LogMD5); end; procedure TFormMain.LogMD5(http: TW3HttpRequest); begin var resp := TMD5Response(JSON.Parse(http.ResponseText)); Log("MD5('" + resp.original + "') = " + resp.md5); end;