Creation of API

We saw how to Load data into a model and how to compute statistical indicators and print it in a terminal. Question is, how can this indicator be exploited outside of GreyCat and/or by third party applications. The obvious answer is through an API.

Exposing function

Exposing a function in GreyCat is as simple as adding an annotation on top of the function declaration. It is important to note that only module functions can be exposed (i.e. functions that are declared outside of a type). To expose publicly the function providing the statistics of bikes per hour, we add two annotations to the function declaration.

@expose
@permission("public")
fn bikesPerHour() {
[...]

@expose instructs GreyCat to expose the function as an API, @permission(“public”) instructs GreyCat that this function can be accessed publicly. By default, exposed functions are only available to authenticated users.
Now, we need to adapt a bit our function to return a result. The return type is set to Table and a return statement replace the console println.

@expose
@permission("public")
fn bikesPerHour(): Table {
[...]
    //Display result in console
    //println(result);
    return result;
}

APIs are exposed to an address mirroring the path to the file (module) from the project.gcl location, followed by the name of the function. In our case, the function being in the project.gcl module at the root of the project, the curl request looks like curl -X POST http://localhost:8080/project::bikesPerHour where the /project is the module and ::bikesPerHour is the function. If the function would be in a module called stations.gcl itself in an api folder, the curl would be curl -X POST http://localhost:8080/api/stations::bikesPerHour.

Testing the API

Stop your greycat instance, and change its running mode to serve so GreyCat does not just execute the script, but serves the application. Use greycat serve. When ready, GreyCat will display

INFO  2023-10-24T09:12:34.194345+00:00 GreyCat is serving on port: 8080

You can now call the API via *Curl for instance: curl -X POST http://localhost:8080/project::bikesPerHour. You should receive the table in the body of the request.

API parameters

For our function providing the bikes availabilities statistics, it would be nice to be able to specify which day and/or which station is requested. To this end, we will add parameters to the function. The day will be specified by a mandatory int value, 0 for Sunday, 6 for Saturday; the name of the station will be an optional (nullable) parameter. If null, all stations will be provided.
We also extract the part of code filling the table in another function to factor the code. The API function now looks like this.

@expose
@permission("public")
fn bikesPerHour(day: int, stationName: String?): Table {
    if(day < 0 || 6 < day) {
        throw "The value for day must be in range 0-Sunday..6-Saturday. Got: ${day}";
    }

    var result = Table::new(25); //One per hour plus station name
    if(stationName != null) {
        var stationNode = stations_by_name.get(stationName);
        if(stationNode == null) {
            throw "Station not found with name '${stationName}'";
        } else {
            fillTableWithProfile(day, result, stationNode);
        }
    } else {
        //For each station
        for(stationName, stationNode in stations_by_name) {
            fillTableWithProfile(day, result, stationNode);
        }
    }
    return result;
}

fn fillTableWithProfile(day: int, result: Table, stationNode: node<Station>) {
    var baseSlot = day * 24;
    var endSlot = (day+1) * 24;
    var tableLine = result.rows();
    //Set the name of the station in first colum of current line
    result.set(tableLine, 0, stationNode->name);
    //Get available bikes profile and resolve it (to not resolve each time)
    var availableBikesProfile = *stationNode->available_bikes_profile;
    //Fill the remaining columns with the average number of bikes, reduced to an integer
    var col = 1;
    var currentSlot = baseSlot;
    while(currentSlot < endSlot) {
        result.set(tableLine, col, availableBikesProfile.avg(currentSlot) as int);
        currentSlot++;
        col++;
    }
}

We also added protections throwing errors to help users correct their requests, on the day and stationName parameters.

Let’s try to get, as before, the profiles of all stations on Thursday (day=4)

% curl -X POST http://localhost:8080/project::bikesPerHour -d '[4,null]' 
{
"_type":"core.Table",
"meta":[...],
"data":[
    ["154 - PLASKY/PLASKY",19,19,19,19,19,19,19,17,15,17,16,17,17,17,17,16,15,13,12,11,12,11,11,15],
    ["206 - SAINT-GUIDON / SINT GUIDO",7,7,7,7,7,6,6,6,7,7,7,7,7,7,6,6,6,6,6,6,6,6,6,6],
    ...
]}

Let’s try to have the profile of Thursday for only one station selected randomly

% curl -X POST http://localhost:8080/project::bikesPerHour -d '[4,"347 - ERASME"]'
{
"_type":"core.Table",
"meta":[...],
"data":[["347 - ERASME",15,15,15,15,15,15,15,15,15,15,15,15,14,14,15,15,15,14,14,14,14,14,14,14]]
}

Let’s try to have the profile of Monday for only one station selected randomly

% curl -X POST http://localhost:8080/project::bikesPerHour -d '[1,"347 - ERASME"]'
{
"_type":"core.Table",
"meta":[...],
"data":[["347 - ERASME",0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]]
}

Let’s try values out of bounds

% curl -X POST http://localhost:8080/project::bikesPerHour -d '[-1,"347 - ERASME"]'
{
    "_type":"core.Error",
    "code":{
        "_type":"core.ErrorCode",
        "field":"throw"
    },
    "value":"The value for day must be in range 0-Sunday..6-Saturday. Got: -1",
    "stack":["bikesPerHour (project.gcl:101:86)"]
}

Let’s try an unknown station

% curl -X POST http://localhost:8080/project::bikesPerHour -d '[0,"GreyCat"]'
{
    "_type":"core.Error",
    "code":{
        "_type":"core.ErrorCode",
        "field":"throw"
    },
    "value":"Station not found with name 'GreyCat'",
    "stack":["bikesPerHour (project.gcl:108:66)"]
}