Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

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"
}
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 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:

...

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.

...

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:

...

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. 

...

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"

...

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). 

...

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:

...

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.

...