/
REST APIs - FIN5 as a Client

REST APIs - FIN5 as a Client

Introduction

This document provides some practical example usage of how to implement REST APIs in FIN, using Fantom programming language.

Prerequisites

Source code

The full source code for this example is contained in the following ZIP file.

OpenHolidays example

OpenHolidays API is a small Open Data project that collects public holiday and school holiday data and makes it available via an open REST API interface.

The following example will showcase how to:

  • read data from the OpenHolidays endpoints, to get the holidays for a specific city

  • use this data to build a scheduler: we want, for example, to set to "off" or false a set of points on scheduled holidays

Disclaimer

Do not use this architecture / structure to update the curVal of points. To do that, you should write a custom connector.

WebClient and basic HTTP utilities

In fan/http  you can find the Http.fan  utility class that contains two functions that allow getting and posting JSON to a REST endpoint.

using haystack using web using skyarcd using util ** ** HTTP utilities ** const class Http { static Obj? postJson(Uri url, Str reqBody, Dict headers := Etc.emptyDict) { c := WebClient(url) if (RestExampleExt.cxLog.isDebug) { RestExampleExt.cxLog.debug("Posting JSON @ ${url} ${reqBody}...") } try { c.reqMethod = "POST" setHeaders(c, headers) c.reqHeaders[CONTENT_TYPE] = JSON_MIME_TYPE c.reqHeaders[CONTENT_LENGTH] = "$reqBody.size" c.writeReq.reqOut.print(reqBody).close c.readRes if (c.resCode != 200 && c.resCode != 202){ if (c.resCode == 401 || c.resCode == 403) { throw IOErr("Not authorized") } throw IOErr("Bad HTTP response $c.resCode $c.resPhrase $c.resBuf.readAllStr") } return JsonInStream(c.resIn).readJson } catch (Err e) { RestExampleExt.cxLog.err("POST request failed", e) throw e } finally { c.close } } static Obj? getJson(Uri url, Dict headers := Etc.emptyDict) { c := WebClient(url) if (RestExampleExt.cxLog.isDebug) { RestExampleExt.cxLog.debug("Getting JSON from ${url}...") } try { c.reqMethod = "GET" setHeaders(c, headers) c.writeReq.readRes if (c.resCode != 200) { if (c.resCode == 401 || c.resCode == 403) { throw IOErr("Not authorized") } throw IOErr("Bad HTTP response $c.resCode $c.resPhrase") } return JsonInStream(c.resIn).readJson } catch (Err e) { RestExampleExt.cxLog.err("GET request failed", e) throw e } finally { c.close } } private static Void setHeaders(WebClient c, Dict headers := Etc.emptyDict) { headers.each |Obj? val, Str key| { if (val != null) { c.reqHeaders.add(key, val.toStr) } } } ** ** Zinc MIME type. ** static const Str ZINC_MIME_TYPE := "text/zinc" ** ** JSON MIME type. ** static const Str JSON_MIME_TYPE := "application/json" ** ** Hayson MIME type. ** static const Str HAYSON_MIME_TYPE := "application/vnd.haystack+json" ** ** Older Haystack JSON format. ** static const Str V3_JSON_MIME_TYPE := "application/vnd.haystack+json;version=3" ** ** Content type HTTP header ** static const Str CONTENT_TYPE := "Content-Type" ** ** Content length HTTP header ** static const Str CONTENT_LENGTH := "Content-Length" }

Getting JSON

Let's start from the GET function.

The function accepts two parameters:

  • the url of the endpoint to call

  • a Dict  where you can specify custom headers: this is particularly useful when you want to add authentication headers, for example

static Obj? getJson(Uri url, Dict headers := Etc.emptyDict) { c := WebClient(url) if (RestExampleExt.cxLog.isDebug) { RestExampleExt.cxLog.debug("Getting JSON from ${url}...") } try { c.reqMethod = "GET" setHeaders(c, headers) c.writeReq.readRes if (c.resCode != 200) { if (c.resCode == 401 || c.resCode == 403) { throw IOErr("Not authorized") } throw IOErr("Bad HTTP response $c.resCode $c.resPhrase") } // Parse the JSON response return JsonInStream(c.resIn).readJson } catch (Err e) { RestExampleExt.cxLog.err("GET request failed", e) throw e } finally { c.close } }

 

What this function does is:

  • instantiating a WebClient that points to the URL to invoke

  • setting the request method to "GET"

  • setting the HTTP headers

  • executing the request

  • reading the response code to understand the outcome of the request

  • parsing the response stream to JSON: this will return a map or a list of maps Str:Obj 

Since we don't know if the API will return an object or an array, the result type of this function is Obj? . Returning an array is not a good practice in REST API development, but there are many implementations that still do it. It's responsability of who is using this function to properly cast the returned object (we will see an example later).

This should all look familiar to anyone who had to develop a REST API.

 

Posting JSON

Let's see how the POST works. It accepts one more parameter, which will be a String representing the JSON body of the request.

static Obj? postJson(Uri url, Str reqBody, Dict headers := Etc.emptyDict) { c := WebClient(url) if (RestExampleExt.cxLog.isDebug) { RestExampleExt.cxLog.debug("Posting JSON @ ${url} ${reqBody}...") } try { c.reqMethod = "POST" setHeaders(c, headers) c.reqHeaders[CONTENT_TYPE] = JSON_MIME_TYPE c.reqHeaders[CONTENT_LENGTH] = "$reqBody.size" // Write request body c.writeReq.reqOut.print(reqBody).close c.readRes if (c.resCode != 200 && c.resCode != 202){ if (c.resCode == 401 || c.resCode == 403) { throw IOErr("Not authorized") } throw IOErr("Bad HTTP response $c.resCode $c.resPhrase $c.resBuf.readAllStr") } // Read the JSON response return JsonInStream(c.resIn).readJson } catch (Err e) { RestExampleExt.cxLog.err("POST request failed", e) throw e } finally { c.close } }

 

The POST function is very similar to the GET function. It adds some additional standard headers, like "Content-Type: application/json", and "Content-Length": the length of the body is necessary for the WebClient to properly send out the request body.

Then, the body of the request is written and the request is sent out.

As before, we can then read the JSON response.

So, Http.fan  contains two generic example functions to GET and POST data to an external endpoint. We can write similar functions for the other operations (PUT, PATCH and DELETE).

 

OpenHolidaysClient

OpenHolidaysClient.fan  is a class that acts as a REST client for the OpenHolidays APIs. It contains the implementation of that specific APIs, making use of the Http.fan  utilities to get the data.

 

It implements two specific APIs of OpenHolidays:

  • getSubdivisions : given a country as a 2-letter ISO country code, returns the subdivisions of that country, that are regions/cities. Each subdivision is identified by a code. We will use this API to find the subdivision code that represents the city where our FIN site is placed.

 

  • getPublicHolidays : given a country as a 2-letter ISO country code and a timerange, returns the list of public holidays applicable. Each holiday can be National or Local. If it's Local, it's only applicable for a specific subdivision.

The other functions in the class are just utility functions to search specific data in the API result.

Let's see one operation in detail:

 

What this function does is:

  • Build the URL by specifying the query parameters to filter the data; the resulting URL will be something like: https://openholidaysapi.org/Subdivisions?countryIsoCode=IT&languageIsoCode=EN

    • languageIsoCode: is the language to use to describe the subdivisions

    • countryIsoCode: is the 2-letter ISO country code

  • Execute the GET call and cast the result as an array of maps [Str:Obj][]

  • Mapping the resulting JSON (array of maps) into an object/class; see the next section

  • Returning the mapped objects as a result

 

Mapping JSON to objects / classes

The result of the GET/POST operations is a map, or an array of maps. Wouldn't it be nice to have an object instead? We can declare a class like Subdivision.fan  and then map the JSON data into the constructor, to build a new object instance:

 

The parameter of the constructor is the JSON map. Then we use an utility function, declared in HttpUtils.fan , that makes use of the reflection to map the JSON data to this object. 

In this way the data structure is clear and documented, and you can easily access the data by using field accessors.

 

At this point we have the APIs implemented, and we can now use the returned data to achieve our goal: create a holidays schedule.

 

Creating a schedule point

A schedule point is a record with specific tags, see scheduleExt.

The most important tag of a schedule record is the scheduleGrid , because it contains the schedule events and rules. An example of schedule grid is:

So each event has a date and timerange, a description, and a value. val  is the value that will be written to the points bound to this scheduler, and can be of any type (string, number, boolean).

Let's see how to create a schedule point:

 

You can create a new record simply by committing a diff (more details on the linked documentation). 

OpenHolidaysService: putting the pieces together

The OpenHolidaysService.fan  is a service class that connects the pieces together: querying the API, transforming the data into schedule events, and creating a schedule record.

The first function accepts as parameters:

  • the timerange of the holidays to query

  • the id of a site

  • the value that the schedule should apply to the points ("val")

  • the current Context, which is an optional parameter (defaults to the current execution Context - it is used to identify the current project and user executing the operations)

The function reads the site to get the "geoCountry" and "geoCity" tags; these two tags will be used as the "isoCountryCode" and as a filter to identify the correct subdivision of the holidays APIs.

Then it GETs the public holidays from the APIs, and map them to a schedule grid. Finally, it creates a schedule point. The point can then be viewed using the Schedules app of FIN.

 

There is just one last missing piece: how can we call this function?

Exposing Fantom functions as Axon functions

Functions and methods created in a Fantom pod can be exposed as Axon functions, that can be called using Folio. 

You just need to use the @Axon  annotation and add the function to the Lib  class of the pod:

 

This Lib class is a special class that is referenced in the index  section of the build.fan , and it is mainly used to expose Fantom functions as Axon functions.

 

Final test

Now we can do a final test and see how to use the functions we developed.

After compiling the pod and restarting FIN, we can create a new site and set the geoCountry and geoCity tags:

image-20250206-131007.png

 

Click on the "i" icon in the top right corner to open the tags view and copy the id of the site.

Then we can open Folio and call the Axon function we exposed in the Lib class:

Example:

testCreateSiteHolidaysSchedule(2025-01-01..2025-12-31, @p:demo:r:2f24d1c7-252b8b94,  "off") 

Replace @p:demo:r:2f24d1c7-252b8b94 with the id of your site. You can also adjust the timerange (2025-01-01..2025-12-31) and the schedule point value ("off") to whatever fits your needs.

 

image-20250206-131022.png

 

If the function call is successful, we can then open the Schedules app and see our newly created schedule point with all the events:

Example usage: a job

To complete the use case, we can imagine to create a job that once a year automatically creates the schedule for the site.

First we need a new Axon function to call from the job, that will simply calculate the date span for the next year, based on today’s date:

 

  • Now open the “Jobs” app in FIN and click on “Create New”

  • Use the following configuration to create a new job that runs once a year and creates the schedule for the next year

  • you can then click on “Run” to run the job for the first time

If you want this function to run on a specific day (example: the 31st of December), you should use Tasks combined with a Scheduler Observable.

Wrap up

This is an example of how to build a REST client in Fantom, implement some APIs, transform and use the data obtained. There is much more that can be done. The suggestion is to consult the documentation of the Fantom language and the Skyspark docs:

 

 

 

 

Related content

FIN HTTP Axon APIs
FIN HTTP Axon APIs
More like this
How to implement third-party services through REST API inside FIN
How to implement third-party services through REST API inside FIN
More like this