How-to: Interaction via HTTP protocol
Example 1
Task
We have a certain set of cities associated with their countries.
CLASS Country 'Country';
id 'Code' = DATA STRING[20] (Country) IN id;
name 'Name' = DATA ISTRING[100] (Country) IN id;
country (STRING[20] id) = GROUP AGGR Country c BY id(c);
CLASS City 'City';
name 'Name' = DATA ISTRING[100] (City) IN id;
country 'Country' = DATA Country (City);
nameCountry 'Country' (City c) = name(country(c));
FORM cities 'Cities'
OBJECTS c = City
PROPERTIES(c) name, nameCountry, NEW, DELETE
;
NAVIGATOR {
NEW cities;
}
We need to send an HTTP request for adding a city in the JSON format to a certain url.
Solution
postCity 'Send' (City c) {
EXPORT JSON FROM countryId = id(country(c)), name = name(c);
LOCAL result = FILE();
EXTERNAL HTTP 'http://localhost:7651/exec?action=Location.createCity' PARAMS exportFile() TO result;
LOCAL code = STRING[10]();
LOCAL message = STRING[100]();
IMPORT JSON FROM result() TO() code, message;
IF NOT code() = '0' THEN {
MESSAGE 'Error: ' + message();
}
}
EXTEND FORM cities
PROPERTIES(c) postCity
;
The EXPORT operator will create a JSON in the FILE format and then will write it to the exportFile property. Here is an example of the generated file:
{"countryId":"123","name":"San Francisco"}
Then we call the EXTERNAL operator, which sends a request to the specified url passing there the contents of the generated file as Body. In this case, since the property in the FROM block has the type JSON, application/json will be used as the content type. <namespace>.<property name> is encoded in the url. In this case, the namespace of the action being called createCity is Location. All parameters are passed consequently with the ID p. The response from the server will be written to the result property. Suppose that the response is received in the JSON format and has one of the following types:
{"code":"0","message":"OK"}
{"code":"1","message":"Invalid country code"}
The response is handled by the IMPORT operator which parses the corresponding parameters into the code and message properties respectively. If any error occurs, the user will see a corresponding error message.
Example 2
Task
Similar to Example 1.
We need to handle the incoming HTTP request and create a new city in the database with the parameters provided in the request.
Solution
createCity (FILE f) {
LOCAL cy = STRING[20] ();
LOCAL ne = STRING[100] ();
IMPORT JSON FROM f AS FILE TO() cy = countryId, ne = name;
IF NOT country(cy()) THEN
RETURN JSON FROM code = '1', message = 'Invalid country code';
NEW c = City {
name(c) <- ne();
country(c) <- country(cy());
APPLY;
}
RETURN JSON FROM code = '0', message = 'OK';
}
Since the property is named createCity and located in the module with the namespace Location, the url on which the request will be handled looks like this:
http://localhost:7651/exec?action=Location.createCity
Body of the HTTP request will be passed as a parameter of the type FILE. The values read from the countryId and name parameters are written to the local properties cy and ne respectively.
If there is no country with the corresponding code, then the RETURN operator aborts execution and returns a JSON similar to that described in the previous example. This JSON is built by the JSON operator, and by default the value of RETURN is passed in the response (see results).
If all the actions are completed successfully, the corresponding "OK message" is returned in response.
Example 3
Task
We have the logic of book orders.
CLASS Book 'Book';
id 'Code' = DATA STRING[10] (Book) IN id;
name 'Name' = DATA ISTRING[100] (Book) IN id;
book (STRING[10] id) = GROUP AGGR Book b BY id(b);
CLASS Order 'Order';
date 'Date' = DATA DATE (Order);
number 'Number' = DATA STRING[10] (Order);
CLASS OrderDetail 'Order line';
order 'Order' = DATA Order (OrderDetail) NONULL DELETE;
book 'Book' = DATA Book (OrderDetail) NONULL;
nameBook 'Book' (OrderDetail d) = name(book(d));
quantity 'Quantity' = DATA INTEGER (OrderDetail);
price 'Price' = DATA NUMERIC[14,2] (OrderDetail);
FORM order 'Order'
OBJECTS o = Order PANEL
PROPERTIES(o) date, number
OBJECTS d = OrderDetail
PROPERTIES(d) nameBook, quantity, price, NEW, DELETE
FILTERS order(d) = o
EDIT Order OBJECT o
;
FORM orders 'Orders'
OBJECTS i = Order
PROPERTIES(i) READONLY date, number
PROPERTIES(i) NEWSESSION NEW, EDIT, DELETE
;
NAVIGATOR {
NEW orders;
}
We need to send an HTTP request for creating a new order in the JSON format to a certain url.
Solution
FORM exportOrder
OBJECTS order = Order PANEL
PROPERTIES dt = date(order), nm = number(order)
OBJECTS detail = OrderDetail
PROPERTIES id = id(book(detail)), qn = quantity(detail), pr = price(detail)
FILTERS order(detail) = order
;
exportOrder 'Send' (Order o) {
EXPORT exportOrder OBJECTS order = o JSON;
LOCAL result = FILE();
EXTERNAL HTTP 'http://localhost:7651/exec?action=Location.importOrder' PARAMS exportFile() TO result;
}
EXTEND FORM orders
PROPERTIES(i) exportOrder;
;
To create a JSON with nested tags, we need to create a form with the corresponding objects linked via the FILTERS block of operators. Based on the dependencies between objects, the system will generate a JSON file with the corresponding structure. In the considering example, the output JSON structure will look like this:
{
"dt":"20.08.18",
"nm":"1",
"detail":[
{
"pr":5.99,
"id":"b1",
"qn":3
},
{
"pr":6.99,
"id":"b2",
"qn":2
}
]
}
We do not create a custom tag for order, since the object value is passed as an argument to the EXPORT operator.
In this example, the response to the HTTP request is ignored.
Example 4
Task
Similar to Example 3.
We need to handle the incoming HTTP request and create a new order in the database with the parameters provided in the request.
Solution
date = DATA LOCAL DATE();
number = DATA LOCAL STRING[10]();
id = DATA LOCAL STRING[10] (INTEGER);
quantity = DATA LOCAL INTEGER (INTEGER);
price = DATA LOCAL NUMERIC[14,2] (INTEGER);
FORM importOrder
PROPERTIES dt = date(), nm = number()
OBJECTS detail = INTEGER
PROPERTIES id = id(detail), qn = quantity(detail), pr = price(detail)
;
importOrder (FILE f) {
IMPORT importOrder JSON FROM f;
NEW o = Order {
date(o) <- date();
number(o) <- number();
FOR id(INTEGER detail) DO NEW d = OrderDetail {
order(d) <- o;
book(d) <- book(id(detail));
quantity(d) <- quantity(detail);
price(d) <- price(detail);
}
APPLY;
}
}
To import the corresponding file in the JSON format, we need to create a form of a similar structure, except that the INTEGER type will be used as object classes. During the import process, the tag values will be placed in the properties with the corresponding names. The date and number properties have no parameters, since their values in JSON are provided at the topmost level.
Example 5
Task
Similar to Example 4.
We need to send an HTTP request to create an order in the JSON format to a certain url as in the previous example, except that everything must be wrapped in the order tag.
Solution
GROUP order;
FORM exportOrderNew
OBJECTS o = Order
PROPERTIES IN order dt = date(o), nm = number(o)
OBJECTS detail = OrderDetail IN order
PROPERTIES id = id(book(detail)), qn = quantity(detail), pr = price(detail)
FILTERS order(detail) = o
;
exportOrderNew 'Send (new)' (Order o) {
EXPORT exportOrderNew OBJECTS o = o JSON;
LOCAL result = FILE();
EXTERNAL HTTP 'http://localhost:7651/exec?action=Location.importOrderNew' PARAMS exportFile() TO result;
}
EXTEND FORM orders
PROPERTIES(i) exportOrderNew;
;
Unlike the previous example, here we create a property group named order using the GROUP operator. When declaring a form, we put all the properties of the purchase order as well as the detail object into this property group. The result JSON will look like this:
{
"order":{
"dt":"20.08.18",
"nm":"1",
"detail":[
{
"pr":5.99,
"id":"b1",
"qn":3
},
{
"pr":6.99,
"id":"b2",
"qn":2
}
]
}
}
Example 6
Task
Similar to Example 5.
We need to handle the incoming HTTP request and create a new order in the database with the parameters provided in the request.
Solution
FORM importOrderNew
PROPERTIES IN order dt = date(), nm = number()
OBJECTS detail = INTEGER IN order
PROPERTIES id = id(detail), qn = quantity(detail), pr = price(detail)
;
importOrderNew (FILE f) {
IMPORT importOrderNew JSON FROM f;
NEW o = Order {
date(o) <- date();
number(o) <- number();
FOR id(INTEGER detail) DO NEW d = OrderDetail {
order(d) <- o;
book(d) <- book(id(detail));
quantity(d) <- quantity(detail);
price(d) <- price(detail);
}
APPLY;
}
}
Just as in the export process, we put all the properties and the detail object to the order group to correctly receive the new version of JSON.
Example 7
Task
Similar to Example 3.
We need to return a list of order numbers for a given date using an HTTP GET request in which this date is provided.
Solution
FORM exportOrders
OBJECTS date = DATE PANEL
OBJECTS order = Order
PROPERTIES nm = number(order)
FILTERS date(order) = date
;
getOrdersByDate (DATE d) {
RETURN JSON(exportOrders OBJECTS date = d);
}
The url to which the HTTP request should be sent will look like this: http://localhost:7651/exec?action=Location.getOrdersByDate&p=12.11.2018.
The response JSON will look like this:
{
"order": [
{
"nm": "42"
},
{
"nm": "65"
}
]
}
If there are no orders on that date, the JSON operator returns NULL, and the response comes back with an empty body. If the receiver needs a JSON with an empty list ({"order":[]}), the form is exported to a file with the EXPORT operator, and that file is returned:
getOrdersByDateFile (DATE d) {
LOCAL f = JSONFILE ();
EXPORT exportOrders OBJECTS date = d JSON TO f;
RETURN f();
}
A result in a format other than JSON is returned the same way: for example, EXPORT CSV FROM ... TO f followed by RETURN f().
Example 8
Task
Similar to [Example 3](#example 3).
For each order, there is a list of files attached to it.
CLASS Attachment 'Attachment';
order = DATA Order (Attachment) NONULL DELETE;
name 'Name' = DATA STRING (Attachment);
file = DATA FILE (Attachment);
We need to implement HTTP GET request that will return for the passed internal identifier of the order its parameters and a list of files. In addition, we need to implement a separate request to retrieve the content of a specific file.
Solution
FORM orderAttachments
OBJECTS o = Order
PROPERTIES(o) number, date
OBJECTS attachments = Attachment
PROPERTIES id = VALUE(attachments), name(attachments)
FILTERS order(attachments) = o
;
getOrderAttachments (LONG orderId) {
FOR LONG(Order o AS Order) = orderId DO
RETURN JSON(orderAttachments OBJECTS o = o);
}
To generate an HTTP request, use the following url : http://localhost:7651/exec?action=getOrderAttachments&p=32178. In it p parameter contains internal order identifier.
The response will return for example the following JSON :
{
"date": "20.10.2021",
"number": "12",
"attachments": [
{
"name": "File 1",
"id": 32180
},
{
"name": "File 2",
"id": 32183
}
]
}
The id attributes will contain internal file identifiers. The contents of these files can then be read by a query using the following url : http://localhost:7651/exec?action=getOrderAttachment&p=32180. The getOrderAttachment action is declared as follows :
getOrderAttachment (LONG id) {
FOR LONG(Attachment a AS Attachment) = id DO
RETURN file(a);
}
The query will return a file with Content-Type corresponding to the extension of the file property.
Example 9
Task
Similar to Example 1.
During development we need to run a fragment of lsFusion code on a running server — for example, to count the cities of each country — without adding it to a module and restarting the server.
Solution
The code is sent in the body of a POST request to the /eval or /eval/action endpoint of the HTTP API, with a content type other than application/x-www-form-urlencoded:
curl -X POST -H "Content-Type: text/plain; charset=utf-8" --data-binary @script.lsf http://localhost:7651/eval
The server wraps the received text into a module of its own that depends on every loaded module, so the text has no MODULE or REQUIRE header, and the elements of the project are referred to by name. The shape of the text depends on the endpoint:
-
/eval— a sequence of module statements, one of which declares an action namedrun. The other statements may declare properties and actions that it uses, among them anINTERNALaction with an inline Java fragment, which has no action statement form and is called fromrun[]:cityCount (Country c) = GROUP SUM 1 IF country(City ct) = c;
logCount INTERNAL <{ System.out.println("counting cities"); }>;
run() {
logCount();
RETURN JSON FROM name = name(Country c), cities = cityCount(c);
} -
/eval/action— the body of that action alone: action statements, local property declarations included. A module header, an action declaration and an operator form that declares a standalone action —INTERNALwith a class name or an inline Java fragment, for example — are not allowed here (missing EOF at 'MODULE',no viable alternative at input '{'or'<{ ... }>'): such a declaration goes into a/evalscript, as above:LOCAL count = INTEGER ();
count() <- GROUP SUM 1 IF c IS City;
RETURN count();
By default the response carries the value of RETURN (see results). RETURN delivers its value from any place in the action, a NEWSESSION block and a point after APPLY included. An APPLY cancelled by a constraint does not fail the request: the changes are not saved and the action goes on, so without the check below the example would return the count with the unsaved city. The action checks canceled[] right after APPLY and, if the apply was cancelled, returns the text of applyMessage[]:
run() {
NEWSESSION {
NEW ct = City {
name(ct) <- 'Springfield';
}
APPLY;
IF canceled() THEN RETURN applyMessage();
RETURN JSON FROM cities = GROUP SUM 1 IF c IS City;
}
}
Example 10
Task
Similar to Example 1.
The service that receives the cities is not always available: it answers 429 Too Many Requests when called too often and 503 Service Unavailable while it restarts, and at times the connection to it cannot be opened at all. The request must be repeated several times with a growing pause before the error is shown.
Solution
postCity 'Send' (City c) {
EXPORT JSON FROM countryId = id(country(c)), name = name(c);
LOCAL result = FILE();
LOCAL responseHeaders = TEXT (TEXT);
LOCAL attempt = INTEGER ();
LOCAL pause = LONG ();
LOCAL wait = LONG ();
LOCAL sent = BOOLEAN ();
timeoutHttp() <- 10000; // no response within 10 s is an error, repeated like a failed connection
attempt() <- 1;
pause() <- 1000;
WHILE NOT sent() AND attempt() <= 5 DO {
statusHttp() <- NULL; // a failed connection or a timeout leaves the status unwritten
responseHeaders(TEXT h) <- NULL WHERE responseHeaders(h); // the headers of the previous attempt
TRY {
EXTERNAL HTTP 'http://localhost:7651/exec?action=Location.createCity' HEADERSTO responseHeaders PARAMS exportFile() TO result;
sent() <- TRUE;
} CATCH {
// 429 and 5xx, like a failed connection (NULL status), are worth another attempt; any other status is final
IF (statusHttp() < 500 AND statusHttp() != 429) OR attempt() = 5 THEN
throwException(messageCaughtException());
// the pause: Retry-After (a number of seconds, whatever the spelling of the header name) when the service sends it, otherwise the doubling pause
wait() <- OVERRIDE LONG(GROUP MAX responseHeaders(TEXT h) IF lower(h) = 'retry-after') * 1000, pause();
sleep(wait());
pause() <- pause() * 2;
attempt() <- attempt() + 1;
}
}
LOCAL code = STRING[10]();
LOCAL message = STRING[100]();
IMPORT JSON FROM result() TO() code, message;
IF NOT code() = '0' THEN {
MESSAGE 'Error: ' + message();
}
}
EXTEND FORM cities
PROPERTIES(c) postCity
;
The loop makes up to five attempts. A response with a status outside 2xx and a failed connection both end the EXTERNAL operator with an error, which the TRY operator intercepts. Each attempt starts by clearing statusHttp[] and the captured headers, so that both belong to that attempt alone: the status is written only when a response has arrived, so after a failed connection or a timeout it stays NULL, and a header absent from the response would otherwise keep the value captured by an earlier attempt. For a response with a status outside 2xx, the response body, the captured headers and the status are written before the error is raised, so the recovery action sees them. The statuses 429 and 5xx, like a missing status, mean that the request is worth repeating. A missing status means only that this attempt recorded no status. The service may already have processed the request, so a request is repeated this way only when repeating it is harmless or the service recognizes a repeated request. Any other status, and the failure of the fifth attempt, is final: throwException[TEXT] ends the action with a new error carrying the message of the caught one. The pause before the next attempt is the value of the Retry-After header when the service sends it as a number of seconds. When the header is absent or its value is not a number (the header may also carry an HTTP date), the conversion to LONG gives NULL and the pause of that attempt is used instead: 1, 2, 4 and 8 seconds. The platform keeps the header names in their original spelling, so they are lowercased for the Retry-After lookup. sleep[LONG] (System) pauses the action itself for that time. timeoutHttp[] sets the connection and response timeouts of every attempt to 10 seconds, so a call that gets no response within that time fails and is repeated like a failed connection.