Accessing data from web services
Learn how to access data from web services.
If you want to try this example, you can deploy the following endpoint:
Tutorial 12 - Querying web services
- Overview
- Code
Usage:
/tutorial/12-query-web-services
let
latitude = "46.5197",
longitude = "6.6323",
geoQuery = latitude + "+" + longitude,
// Put your OpenCage API key here
key = Environment.Secret("OPENCAGE_API_KEY"),
httpQuery = Http.Get(
"https://api.opencagedata.com/geocode/v1/json",
headers = [{"Accept", "application/json"}],
args = [{"q", geoQuery}, {"key", key}, {"no_annotations", "1"}]
),
data = Json.InferAndRead(httpQuery, preferNulls = true)
in
data.results.formatted
Quick Summary
- The HTTP package builds locations which are then used in conjunction with JSON or CSV packages to parse the result.
- Paginated results can be handled with recursive functions.
Querying Web Services
Snapi supports reading data directly from web services. For this, you will need to use the HTTP package, and usually the JSON or CSV packages to parse the result.
To do so, you must first build an HTTP location to make the request.
For example, the following code builds an HTTP GET request. The args
argument takes a list of name/value for the query parameters, while the headers
argument receives the list of headers to pass in the request.
Http.Get(
"https://example.org/web-service",
args = [{"parameter", "value"}],
headers = [{"Accept", "text/json"}]
)
Calling Http.Get
does not directly trigger the request. Instead, it builds a location.
The request is only executed when data is read using e.g. Json.Read
or Csv.Read
, as in:
let
location = Http.Get(
"https://example.org/web-service",
args = [{"parameter", "value"}],
headers = [{"Accept", "text/json"}]
)
in
Json.Read(location, type collection(int))
The program above does an HTTP GET request, passing query parameters and headers, and parses the output as a JSON structure.
This assumes the response of the HTTP web service was 200.
If you would like to configure "valid codes", refer to the expectedStatus
optional parameter of the Http
methods:
the default value is [200]
, which means only the 200 code is accepted by default to indicate a success.
Every other response will trigger an error, which can also be handled: refer to the Error Handling guide for more information.
Alternatively, you can also use Http.Read
which returns a record with the response body and the status code.
Handling Pagination
Web services can produce paginated results. For instance, a JIRA server exposes multiple REST APIs that offer programmatic access to its database. The responses are in JSON.
A call to JIRA's search
REST API returns the set of issues matching a given search criteria.
Its results are paginated.
If you want to try this example, you can deploy the following endpoint:
Querying a JIRA server
- Overview
- Code
Usage:
/jira
For instance, with query fixVersion=9.0.0
query, jql
returns all JIRA issues fixed in version 9.0.0:
[
{
"key": "JRASERVER-73294",
"summary": "Update product documentation for Zero Downtime Upgrade (ZDU) related steps",
"status": "Closed",
"resolutiondate": "2022-11-22T14:25:58.000+0000"
},
{
"key": "JRASERVER-74200",
"summary": "Improve the Archiving a project and Archiving an issue documentation to account for the need of a re-index to assertively decrease Index size (and disk space)",
"status": "Closed",
"resolutiondate": "2022-11-22T14:18:20.000+0000"
},
{
"key": "JRASERVER-74506",
"summary": "Product document Running Jira applications over SSL or HTTPS has incorrect step for command line installation",
"status": "Closed",
"resolutiondate": "2022-11-21T10:05:10.000+0000"
},
...
...
...
{
"key": "JRASERVER-72995",
"summary": "Jira webhooks stop working after \"I/O reactor terminated abnormally\" error",
"status": "Closed",
"resolutiondate": "2022-03-31T10:35:40.000+0000"
},
{
"key": "JRASERVER-73252",
"summary": "Restarting the database causes cache replication issues",
"status": "Closed",
"resolutiondate": "2022-03-31T10:32:55.000+0000"
}
]
jql(projectURL: string, query: string) =
// Type of the JSON returned by JIRA's search API to describe an issue.
let
issueType = type record(
expand: string,
id: string,
self: string,
key: string,
fields: record(
statuscategorychangedate: string,
issuetype: record(
self: string,
id: string,
description: string,
iconUrl: string,
name: string,
subtask: bool,
avatarId: int,
hierarchyLevel: int
),
timespent: undefined,
project: record(
self: string,
id: string,
key: string,
name: string,
projectTypeKey: string,
simplified: bool,
avatarUrls: record(`48x48`: string, `24x24`: string, `16x16`: string, `32x32`: string)
),
fixVersions: collection(undefined),
aggregatetimespent: undefined,
resolution: undefined,
customfield_10630: undefined,
customfield_10631: undefined,
customfield_10621: undefined,
customfield_10500: undefined,
resolutiondate: undefined,
customfield_10627: undefined,
customfield_10628: undefined,
customfield_10629: undefined,
workratio: int,
watches: record(self: string, watchCount: int, isWatching: bool),
lastViewed: undefined,
created: string,
priority: record(self: string, iconUrl: string, name: string, id: string),
customfield_10100: undefined,
labels: collection(string),
customfield_10620: undefined,
customfield_10610: undefined,
customfield_10611: undefined,
customfield_10612: undefined,
customfield_10613: undefined,
timeestimate: undefined,
customfield_10614: undefined,
aggregatetimeoriginalestimate: undefined,
customfield_10615: collection(undefined),
versions: collection(
record(self: string, id: string, name: string, archived: bool, released: bool, description: string)
),
customfield_10616: undefined,
customfield_10617: undefined,
customfield_10618: undefined,
customfield_10619: undefined,
issuelinks: collection(undefined),
assignee: undefined,
updated: string,
status: record(
self: string,
description: string,
iconUrl: string,
name: string,
id: string,
statusCategory: record(self: string, id: int, key: string, colorName: string, name: string)
),
components: collection(record(self: string, id: string, name: string, description: string)),
timeoriginalestimate: undefined,
description: string,
customfield_10600: undefined,
security: undefined,
customfield_10601: undefined,
customfield_10602: undefined,
aggregatetimeestimate: undefined,
customfield_10603: collection(undefined),
customfield_10604: undefined,
customfield_10648: undefined,
customfield_10605: undefined,
customfield_10606: undefined,
customfield_10607: undefined,
customfield_10608: undefined,
customfield_10609: undefined,
summary: string,
creator: record(
self: string,
accountId: string,
avatarUrls: record(`48x48`: string, `24x24`: string, `16x16`: string, `32x32`: string),
displayName: string,
active: bool,
timeZone: string,
accountType: string
),
subtasks: collection(undefined),
reporter: record(
self: string,
accountId: string,
avatarUrls: record(`48x48`: string, `24x24`: string, `16x16`: string, `32x32`: string),
displayName: string,
active: bool,
timeZone: string,
accountType: string
),
aggregateprogress: record(progress: int, total: int),
customfield_10000: string,
customfield_10001: undefined,
customfield_10002: string,
customfield_10200: record(
hasEpicLinkFieldDependency: bool,
showField: bool,
nonEditableReason: record(reason: string, message: string)
),
customfield_10003: undefined,
customfield_10400: undefined,
customfield_10004: undefined,
environment: undefined,
duedate: undefined,
progress: record(progress: int, total: int),
votes: record(self: string, votes: int, hasVoted: bool)
)
),
// Type of the JSON returned by JIRA's search API (a page of results).
jqlType = type record(expand: string, startAt: int, maxResults: int, total: int, issues: collection(issueType)),
rec issues(startAt: int = 0): collection(issueType) =
let
reports = Json.Read(
Http.Get(
projectURL + "/rest/api/latest/search",
args = [{"jql", query}, {"startAt", String.From(startAt)}]
),
jqlType
)
in
if Collection.Count(reports.issues) == 0 then
reports.issues
else
Collection.Union(reports.issues, issues(startAt + 50))
in
issues(0)
main() =
let
v900issues = jql("https://jira.atlassian.com", "fixVersion=9.0.0"),
simplified = Collection.Transform(
v900issues,
(i) ->
{
key: i.key,
summary: i.fields.summary,
status: i.fields.status.name,
resolutiondate: i.fields.resolutiondate
}
)
in
Collection.OrderBy(simplified, (i) -> i.resolutiondate, "DESC")
The whole set of paginated results can be retrieved by looping and calling search
with an increasing startAt
argument.
This should be done until the issues
array returned is empty.
Here is an implementation to handle pagination.
It internally uses a recursive function: note the use of let rec
to indicate the recursive call.
Finally, the results obtained are unioned together into a single collection:
jql(projectURL: string, query: string) =
// recursive function (annotated with rec)
let rec issues(startAt: int = 0): collection(issueType) =
let reports = Json.Read(Http.Get(projectURL + "/rest/api/latest/search", args=[{"jql", query}, {"startAt", String.From(startAt)}]), jqlType)
in
if Collection.Count(reports.issues) == 0
then reports.issues
else Collection.Union(reports.issues, issues(startAt + 50)) // recursive call
in issues(0)