Ask Reuben

Starting With Genero Web Services (part 2 of 3)

How do you expose a function as a Web Service?

What changes do you need to make to your code to expose a function as a Web Service?

How do you call a Web Service?

In the previous article, I covered some key terminology with Genero Web Services.  In this article I am going to show how to create a very basic Web Service and then interact with it.  Using some of the terminology from that article, I will initially be…

  • writing the Server or Provider part of the Web Service equation in the first half of the article and generating a Client / Consumer in the second half of the article.
  • creating a RESTful Web Service
  • using the high-level libraries

I do this by doing something more complex than a Hello World and that is a by exposing an add function that simply adds two number together as a Web Service.    I would suggest having a similar example sitting somewhere that you can always fall back to.  I am creating a web service exposing a function named add that has two input parameters and returns one result parameter.  When it comes to creating your web services, you will have a function with a different name, a different number of input parameters, and a different number of result parameters.

Creating the Server / Provider calculator add example

So lets start with our simple function …

#! calculator_server.4gl
FUNCTION add(a FLOAT, b FLOAT) RETURNS FLOAT
DEFINE c FLOAT

    LET c = a + b
    RETURN c
END FUNCTION

… we need to do two things.

First we add some attributes that will help define this function as a web service.  So this becomes

#! calculator_server.4gl
FUNCTION add(
a FLOAT ATTRIBUTES(WSQuery),       # web service attributes for the input parameters
b FLOAT ATTRIBUTES(WSQuery))
ATTRIBUTES(WSGet, WSPath="/add")   # web service attributes for the function
    RETURNS FLOAT
    DEFINE c FLOAT

    LET c = a + b
    RETURN c
END FUNCTION

For each input parameter, we have added some web service attributes describing the parameter, and then for the function as a whole we have added attributes defining  the function.  I’ll come back to the meaning of these values later.

Second we create a program that will control, expose and listen for requests for this web service.

#! calculator_server_main.4gl
IMPORT com
IMPORT FGL calculator_server

MAIN
DEFINE result INTEGER

    CALL com.WebServiceEngine.RegisterRestService("calculator_server","Calculator")
    CALL com.WebServiceEngine.Start()
    WHILE TRUE
        LET result = com.WebServiceEngine.ProcessServices(-1)
    END WHILE
END MAIN

The com.WebServiceEngine.RegisterRestService method says that the functions are in this module “calculator_server” and that we will expose them under the name “Calculator”.  The com.WebServiceEngine.start method starts listening, and the com.WebServiceEngine.ProcessServices processes the requests as they come in.  If a request comes in with /Calculator in the URL it looks for a function in calculator_server.4gl to process it.

For development purposes with this program, we need to define a port for this program to listen out on, and this is managed by the FGLAPPSERVER environment variable.  The default port number is 8090 and I typically use 8091 hence you will see 8091 in my examples.  If you use a different port, replace 8091 with your number.

Compile and start this program running.  This will be something like

fglcomp calculator_server.4gl
fglcomp calculator_server_main.4gl
export FGLAPPSERVER=8091
fglrun calculator_server_main.42m

This program will now be listening on this port.  Now at the command line type the following (note the quotes so that the & is not interpreted by the command line interpreter, I always forget these) …

curl “http://localhost:8091/ws/r/Calculator/add?a=2&b=3”

You hopefully see 5.0 output.  Experiment changing the values for a and b, and noting the change in output accordingly.

Now type “http://localhost:8091/ws/r/Calculator/add?a=2&b=3” into a browser that has access to this server.  (you may need to replace localhost with the name of your server, but I am hoping you are doing this all on your desktop rather than a remote server).  You will hopefully see 5.0 output like so …



What has happened is that the web service program which is listening on port 8091  has received “/ws/r/Calculator/add?a=2&b=3″ .  From this the /Calculator part tells it to look in calculator_server.4gl, the /add tells it to call the function add(), and a=2 and b=3 tells it what the values of the input parameters are.  That function is called and returns the value 5.0

How does it know what function and what each input parameter is, that is dictated by the Web Service attributes we added to the code.

Function Attributes

For the function attributes, WSGet is the HTTP operation and it is telling it to return something.  WSGet is one of a family of operations.   The default value to use is WSGet and that is because you typically retrieve a resource / select a value from a database.  The other values such as WSPut, WSPost, WSDelete typically relate to wether you are doing the equivalent of an Update, Insert, or Delete respectively.  As a good learning example, you should attempt to create a web service function to maintain a database table, this is where you would see the different HTTP operations in use and their typical relation to a database operation.

The other function attribute we see is WSPath = “/add”.  The WSPath is what provides the mapping from what is in the URL to the function to be called.

When the com.WebServiceEngine.processServices() method is executing, it is interpreting the URL received to determine what module to look in and what function to call from those that have been registered.

Parameter Attributes

For each input parameter, the WSQuery is specifying that the input value to use can be found in the URL after the ?.  Other parameter attributes  we could have used include WSParam which say the parameter is found in a certain position in the URL, WSCookie to say the value was in a Cookie, WSHeader to say the value was in a Header.  Another good learning exercise at this point is to replace WSQuery with these alternate values and figure out how to call the web service.

Other Attributes

There are other attributes for more complex examples, for example uploading and downloading a file as part of a Web Service, error handling, and different types of responses but the above are what you will likely first use when exposing existing functions.  For more about what is in the REST high-level framework refer to the documentationWSMedia, WSAttachment, and some attributes set at the module level will most likely end up in your final production code.


FGLWSDEBUG

When doing this exercise, a good thing to do is to set the FGLWSDEBUG environment variable so that some debug information is written.

With the above program, if we set FGLWSDEBUG =3 before the server program is started, when the Web Service call is made we should get written to stdout/stderr something like …

WS-DEBUG (Receive)
GET /ws/r/Calculator/add?a=2&b=3 HTTP/1.1
WS-DEBUG END

WS-DEBUG (Receive)
Host: localhost:8091
User-Agent: curl/7.64.1
Accept: */*
WS-DEBUG END

WS-DEBUG (Send)
HTTP/1.1 200 Success
WS-DEBUG END

WS-DEBUG (Send)
Content-Type: text/plain; charset=UTF-8
Server: GWS Server (Build 202109140920)
Connection: close
Date: Mon, 21 Feb 2022 11:35:46 GMT
Content-Length: ???
Content-Encoding: ???
WS-DEBUG END

WS-DEBUG (Send)
5.0
WS-DEBUG END

What we can see is what was received /ws/r/Calculator/add?a=2&b=3 and the method GET , and we see that it responds with the result code (200), the code for success in HTTP and the response value 5.0.

If you experiment with the different options WSQuery, WSParam, WSHeader, WSCookie you will see the different ways the same information can be transmitted.  Use man curl to see how to transmit headers, cookies etc via curl.

Code Changes

So hopefully you can see that there is not much coding required to expose a function as a Web Service.  Simply define function and parameter Web Service attributes and create a wrapper program.

However inside your function there is some code you need to review.

User Interface Code

Your function can’t have any code that writes to the user interface.  So no ERROR, INPUT, MENU, DISPLAY ARRAY etc.  So code that mixes business rules and user interface …

FUNCTION field_valid(val INTEGER) RETURNS BOOLEAN

    IF val < 0 THEN
        ERROR "Value must be positive"
        RETURN FALSE
    END IF
    RETURN TRUE
END FUNCTION

... needs to be reviewed and written as something like ...

FUNCTION field_valid(val INTEGER) RETURNS BOOLEAN, STRING

    IF val < 0 THEN
        RETURN FALSE,  "Value must be positive"
    END IF
    RETURN TRUE, NULL
END FUNCTION

Around the turn of the century there were trends to write code that separated business logic and user interface and hopefully your code has had that exercise performed on it or it was written like that in the first place.  If not you might have some untangling.

Modular and Global Variables and Scope

A Web Service function will be placed into a program that is running and listening and service multiple requests.  Any modular and global variables will have their values retained for the life time of the program, they will not be initialised.  It is NOT the case that a modular or global variable is initialised for each request.

To illustrate this, what I suggest is in the above example, add a modular variable DEFINE m INTEGER = 0 outside the add function, and inside the add function add LET m = m + 1, and change the calculation line to LET c = a + b + m.  What you should then observe is that each time the web service is called, the result increments as the modular variable is retained between requests.

Similarly database connection sand database cursors will be retained between requests whilst the program is still alive.

As part of the review process you will normally find that any modular and global variables are reviewed, and that in the main part of the web service function you connect to the database and prepare some database cursors so they are ready for each request.

Process Owner

A web service can serve many different programs.  You should recognise that each request can come in from a different person and so you should make sure there is no security issues whereby some information is retained from the previous request.  This is normally handled by making sure no modular variables.

The other thing you should recognise in this area is that like Web Applications the process owner of the web service process will be an application or system user rather than an individual user.  So any code dependent on HOME or having permissions to write to individual directories should be reviewed, just like it is when moving from Direct connection to GAS.

The while loop

The calculator_server_main.4gl was kept simple.  In the examples in the documentation you will see that there is normally a little more error handling.  Something I'll draw your attention to in the example is note the handling of the -2.  When a web service is idle, the GAS will seek to stop the web service so that an idle web service is not consuming resources unnecessarily.  Hence you see when a -2 is returned, the program exits.


Creating the Client / Consumer example

With a Web Service we may want to create the other side of the equation, that is a program consuming and using that web service.  I will use the same example.

openapi

With the above example running, enter the following URL  "http://localhost:8091/ws/r/Calculator?openapi.json"  either into curl or into the browser similar to the URL executed above.  You should get as ouput ...

{"openapi": "3.0.0","info": {"title": "calculator_server","version": "undefined"} ,"servers": [{"url": "http://localhost:8091/ws/r/Calculator"} ] ,"paths": {"/add": {"get": {"operationId": "add","parameters": [{"in": "query","name": "a","required": true,"schema": {"type": "number","format": "double"} } ,{"in": "query","name": "b","required": true,"schema": {"type": "number","format": "double"} } ] ,"responses": {"200": {"description": "Success","content": {"text/plain": {"schema": {"type": "number","format": "double"} } } } } } } } }

... what this is is the web service self describing itself.  REST has this concept where the web service can be described via json like this example or if you replace .json with .yaml in the previous command as the more human readable yaml

openapi: 3.0.0
info:
  title: calculator_server
  version: undefined
servers:
- url: 'http://localhost:8091/ws/r/Calculator'
paths:
  /add:
    get:
      operationId: add
      parameters:
        - in: query
          name: a
          required: true
          schema:
            type: number
            format: double

        - in: query
          name: b
         required: true
         schema:
          type: number
          format: double
      responses:
        '200':
          description: Success
          content:
            text/plain:
              schema:
                type: number
                format: double

SOAP Web Services have a similar self-describing mechanism that is called WSDL.

fglrestful

Genero has the ability to generate code that calls web services from this description of a Web Service.  if it is a REST Web Service, the command is fglrestful, it is SOAP the command is fglwsdl.

So for our calculator example, with the web service running execute the following code

fglrestful -o calculator_client.4gl "http://localhost:8091/ws/r/Calculator?openapi.json"

That should generate a file named calculator_client.4gl.

Now create a little program that will use that file

#!calculator_client_main.4gl
IMPORT FGL calculator_client

MAIN
DEFINE result FLOAT
DEFINE wsstatus INTEGER

    CALL calculator_client.add(2,3) RETURNING wsstatus , result 
    DISPLAY "Status=", wsstatus
    DISPLAY "Result=", result
END MAIN

Compile this program, and with the web service continuing to run in the background, run this calculator client application.

You should hopefully see a web service status of 0, and a value of 5.0 returned for the add.

If you had FGLWSDEBUG running on your server program, you will hopefully see the what has been passed in and what has been returned and be convinced that the code in the server program was executed.

Code Observations

Error Handling

When calling a Web Service client program, what you should observe is that as in this program you have to worry about the web service status.   You need to detect if the web service was able to be executed and handle the case if the web service could not be executed.

So you might have as a bare minimum ...

IF wsstatus !=0 THEN
    DISPLAY SFMT("An error has occured. Code = %1, Description = %2",
        calculator_client.wsError.code,
        calculator_client.wsError.description)
END IF

Note how the imported module has details of the last error in a modular variable and we are checking that wsstatus is not equal to 0.

High-level vs Low-level

If you want to see the difference between high-level and low-level, have a read of the generated code.  If the Web Service you are seeking to connect with does not provide a URL or file from which you can run fglrestul or fglwsdl to generate, then the style of code you need to use is similar to what you see in this example generated code.

The key thing to note is the use of the com.HTTPRequest and com.HTTPResponse to make a request to the Web Service and to receive a response.

Summary

Using this calculator add example, you have both sides of a simple web service.

I suggest using that as a starting point from which you can experiment with different web service parameters, and more complex input and output parameters.