Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Table of Contents
stylenone

Docs map

Drawio
mVer2
zoom1
simple0
inComment0
custContentId33657159693
pageId33635532809
lbox1
diagramDisplayNameREST API docs
contentVer1
revision1
baseUrlhttps://finproducts.atlassian.net/wiki
diagramNameREST API docs
pCenter0
width1610.0000000000005
links
tbstyle
height1711.9999999999995

FIN as a Client

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.

View file
namerestExampleExt.zip

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

Note

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.

Expand
titleHttp.fan
Code Block
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

Expand
titlegetJson
Code Block
  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.

Expand
titlepostJson
Code Block
  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.

Expand
titleOpenHolidaysClient.fan
Code Block
using haystack

**
** An HTTP client to get info from Open Holidays.
** @see https://www.openholidaysapi.org/en/
**
const class OpenHolidaysClient {

  static const Str EN_ISO_CODE := "EN"
  static const Uri BASE_URI := `https://openholidaysapi.org`
  static const Uri SUBDIVISIONS_URI := BASE_URI + `/Subdivisions`
  static const Uri PUBLIC_HOLIDAYS_URI := BASE_URI + `/PublicHolidays`

  **
  ** Get subdivisions for a specific country code.
  **
  public Subdivision[] getSubdivisions(Str countryIsoCode) {
    try {
      Uri uri := SUBDIVISIONS_URI.plusQuery([
        "countryIsoCode": countryIsoCode.upper,
        "languageIsoCode": EN_ISO_CODE
      ])

      RestExampleExt.cxLog.info("Getting subdivisions @ ${uri}...")
      [Str:Obj?][] response := Http.getJson(uri)

      subdivisions := response.map { Subdivision.make(it) }
      RestExampleExt.cxLog.info("${subdivisions.size} subdivisions found")
      return subdivisions
    } catch (Err e) {
      RestExampleExt.cxLog.err("Error getting subdivisions", e)
      throw e
    }
  }

  **
  ** Find a subdivision by a 2-letters ISO country code and city name.
  **
  public Subdivision? findSubdivisionByCountryAndCity(Str countryIsoCode, Str geoCity) {
    try {
      subdivisions := getSubdivisions(countryIsoCode)
      return findSubdivision(subdivisions, geoCity)
    } catch (Err e) {
      RestExampleExt.cxLog.err("Error finding subdivision $countryIsoCode-$geoCity", e)
      throw e
    }
  }

  **
  ** Get public holidays for a specific country code.
  **
  public PublicHoliday[] getPublicHolidays(Str countryIsoCode, Date from, Date to) {
    try {
      Uri uri := PUBLIC_HOLIDAYS_URI.plusQuery([
        "countryIsoCode": countryIsoCode.upper,
        "languageIsoCode": EN_ISO_CODE,
        "validFrom": from.toStr,
        "validTo": to.toStr
      ])

      RestExampleExt.cxLog.info("Getting public holidays @ ${uri}...")
      [Str:Obj?][] response := Http.getJson(uri)

      holidays := response.map { PublicHoliday.make(it) }
      RestExampleExt.cxLog.info("${holidays.size} holidays found")
      return holidays
    } catch (Err e) {
      RestExampleExt.cxLog.err("Error getting public holidays", e)
      throw e
    }
  }

  **
  ** Find public holidays related to a country (2-letters ISO country code) and an optional city name.
  ** The public holidays are queried for a specific period.
  **
  public PublicHoliday[] findPublicHolidaysByCountryAndCity(Date from, Date to, Str countryIsoCode, Str? geoCity := null) {
    try {
      holidays := getPublicHolidays(countryIsoCode, from, to)
      if (geoCity != null) {
        subdivision := findSubdivisionByCountryAndCity(countryIsoCode, geoCity)

        if (subdivision != null) {
          return holidays.findAll { it.isNational() || it.appliesToSubdivision(subdivision) }
        }
      }

      return holidays.findAll { it.isNational() }
    } catch (Err e) {
      RestExampleExt.cxLog.err("Error finding public holidays $countryIsoCode-$geoCity", e)
      throw e
    }
  }

  private Subdivision? findSubdivision(Subdivision[] subdivisions, Str geoCity) {
    Subdivision? result := subdivisions.eachWhile |Subdivision sub->Subdivision?| {
      if (sub.name.any { geoCity.equalsIgnoreCase(it.text) }) {
        return sub
      } else if (!sub.children.isEmpty) {
        return findSubdivision(sub.children, geoCity)
      }
      return null
    }

    return result
  }
}

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.

Expand
titlegetSubdivision API example
Code Block
[
  {
    "code": "IT-AB",
    "isoCode": "IT-65",
    "shortName": "AB",
    "category": [
      {
        "language": "IT",
        "text": "regione"
      },
      {
        "language": "EN",
        "text": "region"
      },
      {
        "language": "DE",
        "text": "Region"
      }
    ],
    "name": [
      {
        "language": "IT",
        "text": "Abruzzo"
      },
      {
        "language": "EN",
        "text": "Abruzzo"
      },
      {
        "language": "DE",
        "text": "Abruzzen"
      }
    ],
    "officialLanguages": [
      "IT"
    ],
    "children": [
      {
        "code": "IT-AB-AQ",
        "isoCode": "IT-AQ",
        "shortName": "AB-AQ",
        "category": [
          {
            "language": "IT",
            "text": "province"
          },
          {
            "language": "EN",
            "text": "province"
          },
          {
            "language": "DE",
            "text": "Provinz"
          }
        ],
        "name": [
          {
            "language": "IT",
            "text": "L'Aquila"
          },
          {
            "language": "EN",
            "text": "L'Aquila"
          },
          {
            "language": "DE",
            "text": "L'Aquila"
          }
        ],
        "officialLanguages": [
          "IT"
        ]
      },
      {
        "code": "IT-AB-TE",
        "isoCode": "IT-TE",
        "shortName": "AB-TE",
        "category": [
          {
            "language": "IT",
            "text": "province"
          },
          {
            "language": "EN",
            "text": "province"
          },
          {
            "language": "DE",
            "text": "Provinz"
          }
        ],
        "name": [
          {
            "language": "IT",
            "text": "Teramo"
          },
          {
            "language": "EN",
            "text": "Teramo"
          },
          {
            "language": "DE",
            "text": "Teramo"
          }
        ],
        "officialLanguages": [
          "IT"
        ]
      },
      {
        "code": "IT-AB-CH",
        "isoCode": "IT-CH",
        "shortName": "AB-CH",
        "category": [
          {
            "language": "IT",
            "text": "province"
          },
          {
            "language": "EN",
            "text": "province"
          },
          {
            "language": "DE",
            "text": "Provinz"
          }
        ],
        "name": [
          {
            "language": "IT",
            "text": "Chieti"
          },
          {
            "language": "EN",
            "text": "Chieti"
          },
          {
            "language": "DE",
            "text": "Chieti"
          }
        ],
        "officialLanguages": [
          "IT"
        ]
      },
      {
        "code": "IT-AB-PE",
        "isoCode": "IT-PE",
        "shortName": "AB-PE",
        "category": [
          {
            "language": "IT",
            "text": "province"
          },
          {
            "language": "EN",
            "text": "province"
          },
          {
            "language": "DE",
            "text": "Provinz"
          }
        ],
        "name": [
          {
            "language": "IT",
            "text": "Pescara"
          },
          {
            "language": "EN",
            "text": "Pescara"
          },
          {
            "language": "DE",
            "text": "Pescara"
          }
        ],
        "officialLanguages": [
          "IT"
        ]
      }
    ]
  },
  • 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.

Expand
titlegetPublicHolidays API example
Code Block
[
  {
    "id": "1d050a31-4d93-4fde-9ce6-551bed3db5a1",
    "startDate": "2025-01-01",
    "endDate": "2025-01-01",
    "type": "Public",
    "name": [
      {
        "language": "IT",
        "text": "Capodanno"
      },
      {
        "language": "EN",
        "text": "New Year's Day"
      },
      {
        "language": "DE",
        "text": "Neujahr"
      }
    ],
    "regionalScope": "National",
    "temporalScope": "FullDay",
    "nationwide": true
  },
  {
    "id": "28bfa1ac-e3e1-4592-a807-7d092432f9f9",
    "startDate": "2025-01-06",
    "endDate": "2025-01-06",
    "type": "Public",
    "name": [
      {
        "language": "IT",
        "text": "Epifania"
      },
      {
        "language": "EN",
        "text": "Epiphany"
      },
      {
        "language": "DE",
        "text": "Heilige drei Könige"
      }
    ],
    "regionalScope": "National",
    "temporalScope": "FullDay",
    "nationwide": true
  },
  {
    "id": "9e3d16a4-a098-4527-9a31-969f07b3d03c",
    "startDate": "2025-04-21",
    "endDate": "2025-04-21",
    "type": "Public",
    "name": [
      {
        "language": "IT",
        "text": "Lunedì di Pasqua"
      },
      {
        "language": "EN",
        "text": "Easter Monday"
      },
      {
        "language": "DE",
        "text": "Ostermontag"
      }
    ],
    "regionalScope": "National",
    "temporalScope": "FullDay",
    "nationwide": true
  },
  {
    "id": "07bf3f21-4d9c-4dd2-bcd0-5eaf63ee197b",
    "startDate": "2025-04-25",
    "endDate": "2025-04-25",
    "type": "Public",
    "name": [
      {
        "language": "IT",
        "text": "Anniversario della liberazione d'Italia"
      },
      {
        "language": "EN",
        "text": "Liberation Day"
      },
      {
        "language": "DE",
        "text": "Jahrestag der Befreiung"
      }
    ],
    "regionalScope": "National",
    "temporalScope": "FullDay",
    "nationwide": true
  },
  {
    "id": "a377822a-9c21-4866-a2f9-7ebcd03f797a",
    "startDate": "2025-04-25",
    "endDate": "2025-04-25",
    "type": "Public",
    "name": [
      {
        "language": "IT",
        "text": "Festa di San Marco"
      },
      {
        "language": "EN",
        "text": "Feast of St Mark"
      },
      {
        "language": "DE",
        "text": "Fest des Heiligen Markus"
      }
    ],
    "regionalScope": "Local",
    "temporalScope": "FullDay",
    "nationwide": false,
    "subdivisions": [
      {
        "code": "IT-VE-VE",
        "shortName": "VE-VE"
      }
    ]
  },

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:

Expand
titlegetSubdivisions
Code Block
  public Subdivision[] getSubdivisions(Str countryIsoCode) {
    try {
      Uri uri := SUBDIVISIONS_URI.plusQuery([
        "countryIsoCode": countryIsoCode.upper,
        "languageIsoCode": EN_ISO_CODE
      ])

      RestExampleExt.cxLog.info("Getting subdivisions @ ${uri}...")
      [Str:Obj?][] response := Http.getJson(uri)

      subdivisions := response.map { Subdivision.make(it) }
      RestExampleExt.cxLog.info("${subdivisions.size} subdivisions found")
      return subdivisions
    } catch (Err e) {
      RestExampleExt.cxLog.err("Error getting subdivisions", e)
      throw e
    }
  }

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:

Expand
titleSubdivision.fan
Code Block
@Serializable
class Subdivision {
  Str? code
  Str? isoCode
  Str? shortName
  LocalizedText[] category := [,]
  LocalizedText[] name := [,]
  Str[] officialLanguages := [,]
  Subdivision[] children := [,]

  new make (Str:Obj? json) {
    HttpUtils.assignJsonData(json, Subdivision#, this, ["category", "name", "children"])
    if (json["category"] != null) {
      this.category = (([Str:Obj?][])json["category"]).map { LocalizedText(it) }
    }
    if (json["name"] != null) {
      this.name = (([Str:Obj?][])json["name"]).map { LocalizedText(it) }
    }
    childrenSub :=  json["children"]
    if (childrenSub != null) {
      this.children = (([Str:Obj?][]) childrenSub).map { Subdivision(it) }
    }
  }
}

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:

Expand
titlescheduleGrid
Code Block
date,dis,end,start,val
"2025-01-01","New Year's Day",00:00:00,00:00:00,"off"
"2025-01-11","Municipal holiday",00:00:00,00:00:00,"off"
"2025-03-04","Carnival",00:00:00,00:00:00,"off"
"2025-04-18","Good Friday",00:00:00,00:00:00,"off"
"2025-04-20","Easter Sunday",00:00:00,00:00:00,"off"
"2025-04-25","Liberty Day",00:00:00,00:00:00,"off"
"2025-05-01","Labor Day",00:00:00,00:00:00,"off"
"2025-06-08","Corpus Christi",00:00:00,00:00:00,"off"
"2025-06-10","Portugal Day",00:00:00,00:00:00,"off"
"2025-08-15","Assumption Day",00:00:00,00:00:00,"off"
"2025-10-05","Republic Day",00:00:00,00:00:00,"off"
"2025-11-01","All Saints' Day",00:00:00,00:00:00,"off"
"2025-12-01","Independence Day",00:00:00,00:00:00,"off"
"2025-12-08","Immaculate Conception Day",00:00:00,00:00:00,"off"
"2025-12-25","Christmas",00:00:00,00:00:00,"off"

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:

Expand
titleScheduleUtils.fan
Code Block
using folio
using haystack
using skyarc
using skyarcd

const class ScheduleUtils {
  static const Str className := ScheduleUtils#.name

  public Dict createSchedulePoint(Str dis, Grid scheduleGrid, Kind kind, Context cx := Context.cur) {
    Diff diff := Diff.makeAdd([
      "schedule": Marker.val,
      "point": Marker.val,
      "sp": Marker.val,
      "scheduleGrid": scheduleGrid,
      "kind": kind.name,
      "dis": dis,
    ])
    diff = cx.proj.commit(diff)
    return diff.newRec
  }
}

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.

Expand
titleOpenHolidaysService.fan
Code Block
using folio
using haystack
using skyarcd

**
** A service class to create schedule points containing Open Holidays events.
**
const class OpenHolidaysService {
  const OpenHolidaysClient client := OpenHolidaysClient()
  const ScheduleUtils scheduleUtils := ScheduleUtils()

  **
  ** Create a schedule point, reading holidays for the country of a specific site.
  ** The site must have at least the 'geoCountry' tag. The 'geoCity' tag is used if present, but it is optional.
  **
  public Dict createSiteHolidaysSchedule(Date from, Date to, Ref siteId, Obj holidayScheduleVal, Context cx := Context.cur) {
    try {
      Dict site := cx.proj.readById(siteId)

      if (site["geoCountry"] == null) {
        throw Err("Missing 'geoCountry' tag on site $siteId.toCode")
      }

      return createHolidaysSchedule(from, to, site->geoCountry, site["geoCity"], holidayScheduleVal, cx)
    } catch(Err e) {
      RestExampleExt.cxLog.err("Error creating holidays schedule for site $siteId.toCode", e)
      throw e
    }
  }

  **
  ** Create a schedule point, reading holidays for a specific country and an optional city.
  **
  public Dict createHolidaysSchedule(Date from, Date to, Str isoCountryCode, Str? geoCity, Obj holidayScheduleVal, Context cx := Context.cur) {
    try {
      scheduleKind := Kind.fromVal(holidayScheduleVal)
      holidays := client.findPublicHolidaysByCountryAndCity(from, to, isoCountryCode, geoCity)
      // Map holidays to scheduleGrid
      scheduleGrid := Etc.makeDictsGrid(null,
        holidays.map |PublicHoliday holiday->Dict| {
          Etc.makeDict([
            "dis": holiday.name[0].text,
            "val": holidayScheduleVal,
            "date": holiday.startDate,
            "start": Time.defVal,
            "end": Time.defVal,
          ])
        }
      )

      Str place := isoCountryCode + (geoCity != null ? "-${geoCity}" : "")
      Str scheduleDis := "${place} Holidays ${from.toStr} / ${to.toStr}"

      return scheduleUtils.createSchedulePoint(scheduleDis, scheduleGrid, scheduleKind, cx)
    } catch(Err e) {
      RestExampleExt.cxLog.err("Error creating holidays schedule", e)
      throw e
    }
  }
}

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:

Expand
titleRestExampleLib.fan
Code Block
using axon
using haystack
using hisExt
using skyarc
using skyarcd
using util

const class RestExampleLib {

	private static Context cx() { Context.cur }
	static const OpenHolidaysService openHolidaysService := OpenHolidaysService()

[...omissis]

//////////////////////////////////////////////////////////////////////////
// OpenHolidays
//////////////////////////////////////////////////////////////////////////

  **
  ** Create a schedule point containing one event for each holiday retrieved on Open Holidays. Params:
  ** - span: a DateSpan to query for holidays in a date range
  ** - siteId: the id of a site; the site must have at least the 'geoCountry' tag set
  ** - scheduleVal: it's the value the scheduler will set on the holidays (the value to set on the points bound to this schedule point)
  ** Returns the schedule point.
  **
  @Axon
  static Dict testCreateSiteHolidaysSchedule(DateSpan span, Ref siteId, Obj scheduleVal) {
    openHolidaysService.createSiteHolidaysSchedule(span.start, span.end, siteId, scheduleVal, cx)
  }
}

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.

Expand
titlebuild.fan
Code Block
using finBuild

class Args : BuildFinArgs {
  new make() : super(Build#make) {}
}

class Build : BuildFinPod {
  
  new make(Args args) : super(args) {
    podName = "restExampleExt"
    summary = "REST example ext"
    version = Version("1.0.0")
    
    meta = [
      "proj.name":       podName,
      "org.name":        "J2 Innovations",
      "org.uri":         "http://www.j2inn.com/",
      "license.name":    "Commercial",
      "pod.docLocation": "finDoc",
    ]
    
    depends = [
      // Fantom
      "sys        1.0",
      "concurrent 1.0",
      "util       1.0",
      "web        1.0",
      
      // SkySpark
      "skyarc     3.0.25-3.0",
      "skyarcd    3.0.25-3.0",
      "axon       3.0.25-3.0",
      "folio      3.0.25-3.0",
      "haystack   3.0.25-3.0",
      "hisExt     3.0.20+",
    ]
    srcDirs = [
      `fan/`,
      `fan/http/`,
      `fan/http/openholidays/`,
      `fan/http/domain/`,
      `fan/http/domain/openholidays/`,
      `fan/services/`,
      `fan/test/`,
      `fan/utilities/`,
    ]
    resDirs = [`lib/`]
    
    index = [
      "skyarc.ext": "restExampleExt::RestExampleExt",
      "skyarc.lib": "restExampleExt::RestExampleLib",
    ]
  }
}

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:

...

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.pngImage Removed

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:

Expand
titleRestExampleLib.fan
Code Block
  **
  ** Create a schedule point containing one event for each holiday retrieved on Open Holidays, for the next year.
  ** - siteId: the id of a site; the site must have at least the 'geoCountry' tag set
  ** - scheduleVal: it's the value the scheduler will set on the holidays (the value to set on the points bound to this schedule point)
  ** Returns the schedule point.
  **
  @Axon
  static Dict testCreateSiteNextYearHolidaysSchedule(Ref siteId, Obj scheduleVal) {
    nextYear := Date.today.year + 1
    start := Date.make(nextYear, Month.jan, 1)
    end := Date.make(nextYear, Month.dec, 31)
    return openHolidaysService.createSiteHolidaysSchedule(start, end, siteId, scheduleVal, cx)
  }

  • 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

    image-20250211-163835.pngImage Removedimage-20250211-164621.pngImage Removed
  • 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: