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