Taking Multiple into account calling an OpenAPI REST API

AIMMS 4.90 project download

Calling a generated OpenAPI Library, the word “multiple” comes back in several ways:

  1. The service can be provided by multiple servers, each one of them using a unique service URL. For instance, the service can be provided from various regions.

  2. A single service URL can service multiple endpoints, for instance for for finding coordinates of an address, and another for finding an address at some coordinates. Not only are there several endpoints, there are also several schemas for the requests and the responses. It is thus important to find which endpoint to use, and which schemas to fill with data, and which schemas to copy data from.

  3. The response of a single request can have multiple items. For instance, searching for “Church street” will give several results.

  4. Multiple requests are needed to find the locations of cities on a map. For instance, finding an optimal route involves multiple locations.

  5. Rate limits of a service, limit the number of requests per time period. Rate limits often depend on your license.

This how-to article discusses modeling approaches for each of the above. Before going into the details of the above, we present an API to serve as the background of this article.

The Story

The service Search by LocationIQ.com offers, amongst others, the following methods:

  • search: Forward Geocoding: A free form address to Geographical coordinates,

  • Matrix: Compute distance, by time or length, between locations.

Overview

First get an OpenAPI specification of the API at hand.

After that, we will focus on the search method, and do:

  • Making an API Call, we need to put together the method, its inputs, and its outputs.

  • The response of a single request may contain several answers.

  • Making multiple requests, and respecting the rate limit.

The OpenAPI specification

OpenAPI specifications can be found at various places, including APIs.guru, github, and others. The one used in this article was found on Github LocationIQ

Reading tips for an OpenAPI specification

An OpenAPI specification is written in a .json or .yaml file. Although these are text files, I find the following two tools more effective in understanding how to use an OpenAPI specification in an AIMMS application:

  • The site: Swagger editor. After loading the OpenAPI spec file downloaded from github, the search interface shows:

    ../../_images/Swagger-LocationIQ-search.png

    In addition, this editor will show no request body, but a response file for this method.

  • Peek at the code generated for the search method:

    • apiCall: this procedure does not use a request body, but does use several arguments, most of which are optional.

    • callback: this procedure reads in a response body (schema media_type_schema_15, not present in the OpenAPI spec, so generated by DEX). This means that upon success, the identifiers in the schema media_type_schema_15 will be filled when the response hook starts.

      ../../_images/valid-response-uses-media-type-schema.png

The service URL

LocationIQ provides its service from two locations by having two service URL’s:

  1. https://eu1.locationiq.com/v1

  2. https://us1.locationiq.com/v1

You can use one of these values in your ../api-init/openapi-LocationIQ.txt file:

1LocationIQ::api::APIServer :=  "https://eu1.locationiq.com/v1" ;
2LocationIQ::api::APIKey('key') := "<enter your api key here>" ;

The first search call

When searching for a landmark, sometimes the same name is used at multiple places on the globe. For instance, searching for Brandenburg Gate, returns 3 locations, as illustrated in the next image:

../../_images/single-call-multiple-answers.png

The calling code that achieves this is:

1Procedure pr_testWithOpenAPI_1 {
2    Body: {
3        Empty s_locations ;
4        pr_makeRequest( first( s_addressNumbers ) );
5    }
6}

The code for making a request, does not need to take multiple answers into account:

Making the request

Making a request is coded in the procedure locationSearch::pr_makeRequest. Note that this code is structured similarly to the API calling code in the previous article.

 1! Request call instance - used by response hook to determine the request to which the response belongs.
 2LocationIQ::api::NewCallInstance( ep_callInstance );
 3
 4! Fill in the data for making the request.
 5! Nothing here, but data is passed in the LocationIQ::api::search::apiCall arguments.
 6
 7! Fill in the data for administration used inside this module.
 8ep_addresses( ep_callInstance ) := ep_addressNo ;
 9
10! Install hook, which will copy the data or handle the error
11LocationIQ::api::search::UserResponseHook := 'locationSearch::pr_responseHook' ;
12
13! Start the request.
14LocationIQ::api::search::apiCall(
15    callInstance    :  ep_callInstance,
16    q               :  sp_addressString(ep_addressNo),
17    format_         :  'json',
18    normalizecity   :  '1') ;

Remarks:

  • Lines 7,8: The module LocationSearchModule, prefix LocationSearch, uses element parameter LocationSearch::ep_addresses to map each call instance to an address number.

  • Lines 14-18: The apiCall for the LocationIQ method search, passes information in the URL. This information is passed in its arguments. This example only fills the mandatory arguments.

The code for handling a response, however, does need to take multiple answers into account:

Handling the response

Handling a response is coded in the procedure locationSearch::pr_responseHook

 1ep_addr := ep_addresses( ep_callInstance );
 2switch LocationIQ::api::CallStatusCode(ep_callInstance) do
 3    '200':
 4        for LocationIQ::_media_type_schema_15::i_media_type_schema_15 | LocationIQ::_media_type_schema_15::display_name(ep_callInstance, LocationIQ::_media_type_schema_15::i_media_type_schema_15) do
 5
 6            s_locations += card( s_locations ) + 1; ! Get a new location id.
 7            ep_loc := last( s_locations );
 8
 9            ! Copy data from OpenAPI lib.
10            p_lat( ep_loc ) := val( LocationIQ::_media_type_schema_15::lat(ep_callInstance, LocationIQ::_media_type_schema_15::i_media_type_schema_15) );
11            p_lon( ep_loc ) := val( LocationIQ::_media_type_schema_15::lon(ep_callInstance, LocationIQ::_media_type_schema_15::i_media_type_schema_15) );
12            sp_displayName( ep_loc ) := LocationIQ::_media_type_schema_15::display_name(ep_callInstance, LocationIQ::_media_type_schema_15::i_media_type_schema_15);
13
14            ! Copy data from own administration.
15            sp_givenName( ep_loc ) := sp_addressString(ep_addr);
16        endfor ;
17        block ! Cleanup
18            LocationIQ::_media_type_schema_15::EmptyInstance( ep_callInstance );
19            empty ep_addresses( ep_callInstance ); ! Maintaining own administration.
20        endblock ;
21
22    '400','401','403','404','429','500':
23        raise error formatString("LocationIQ/Search(%s) failed. Code: %e, errNo: %i: %s",
24            sp_addressString(ep_addr),
25            LocationIQ::api::CallStatusCode(ep_callInstance),
26            LocationIQ::api::CallErrorCode(ep_callInstance),
27            LocationIQ::_error::error_(ep_callInstance) );
28
29    default:
30        raise error formatString("LocationIQ/Search(%s) failed. Code: %e, errNo: %i: %s",
31            sp_addressString(ep_addr),
32            LocationIQ::api::CallStatusCode(ep_callInstance),
33            LocationIQ::api::CallErrorCode(ep_callInstance),
34            "unknown error" );
35
36endswitch ;

Remarks:

  • Line 4: An array of locations found is returned. We copy only those, that have a display name.

  • Lines 6,7: Every entry gets a location number.

  • Lines 9-12: Copy the data for each location from the OpenAPI generated library.

  • Lines 14,15: Use own administration to fill in the short name of a location.

  • Lines 18,19: Cleanup. Not only information from the response schema, but also from our own administration.

  • Line 22: The status codes for which an error string is filled can be copied easily from the corresponding generated callback call.

  • Lines 23-28, 30-35: Handle an error by passing both information about the call (LocationIQ/Search(%s) filling in sp_addressString(ep_addr)), and information retrieved from the response.

Making multiple requests and respecting rate limits

Two calls

Let’s start with making two calls. Our input data is in locationSearch::sp_addressString(locationSearch::i_addressNo).

1Procedure pr_testWithOpenAPI_2 {
2    Body: {
3        Empty s_locations ;
4        for i_addressNo | ord( i_addressNo ) <= 2 do
5            pr_makeRequest( i_addressNo );
6        endfor ;
7    }
8}

This interpretation of this for loop is that after executing pr_makeRequest( '1' ), the interpreter will directly continue with pr_makeRequest( '2' ). Handling the corresponding responses comes later, after the server finished processing the requests.

Note that handling the response of request 2 may come before handling the response for request 1. This underlines the importance of parameters like ep_addressses; it handles relating responses to their corresponding requests.

Going over the limit

I usually develop with a free API key, so the rate limit is two calls per second. What happens when I go over the limit?

1Procedure pr_testWithOpenAPI_3 {
2    Body: {
3        Empty s_locations ;
4        for i_addressNo | ord( i_addressNo ) <= 3 do
5            pr_makeRequest( i_addressNo );
6        endfor ;
7    }
8}

Well, LocationIQ reports a rate limit exceeded. My error message is as follows: LocationIQ/Search(Brandenburg Gate) failed. Code: 429, errNo: 0: Rate Limited Second.

Accepting a rate limit

Making a request takes almost no time, thus doing at most 2 calls per seconds implies that we need to wait a second after making two requests.

 1Procedure pr_testWithOpenAPI_4 {
 2    Body: {
 3        Empty s_locations ;
 4        for i_addressNo do
 5
 6            pr_makeRequest( i_addressNo );
 7
 8            if mod( ord( i_addressNo ), p_maxRateSecond ) = 0 then
 9                pr_handleResponsesFor( 1[s] );
10            endif ;
11
12        endfor ;
13    }
14    Parameter p_maxRateSecond {
15        InitialData: 2;
16    }
17}

Here the utility procedure pr_handleResponsesFor is coded as follows:

 1Procedure pr_handleResponsesFor {
 2    Arguments: (p_seconds);
 3    Body: {
 4        sp_fmt := "%c%y-%m-%d %H:%M:%S:%T%TZ('UTC')" ;
 5        sp_startTime := CurrentToString( sp_fmt );
 6        p_secondsNoUnit := (p_seconds)[-];
 7        p_milliSeconds := 1000 * p_secondsNoUnit ;
 8        p_responsesHandled := dex::client::WaitForResponses( p_milliSeconds );
 9        while p_responsesHandled do
10            sp_now := CurrentToString( sp_fmt );
11            p_ticks := StringToMoment(
12                Format        :  sp_fmt,
13                Unit          :  [tick],
14                ReferenceDate :  sp_startTime,
15                Timeslot      :  sp_now);
16            if p_ticks > p_seconds then ! Time exceeded. AIMMS handles unit conversions.
17                break ;
18            endif ;
19            p_remainingSeconds := p_seconds - p_ticks ;
20            p_secondsNoUnit :=  (p_remainingSeconds)[-];
21            p_milliSeconds := 1000 * p_secondsNoUnit ;
22            p_responsesHandled := dex::client::WaitForResponses( p_milliSeconds );
23        endwhile ;
24    }
25    Comment: "Wait at least p_second seconds, and handle responses meanwhile.""    DeclarationSection Argument_declarations {
26        Parameter p_seconds {
27            Unit: s;
28            Property: Input;
29        }
30    }
31    DeclarationSection Local_declarations {
32        Parameter p_secondsNoUnit;
33        Parameter p_milliSeconds;
34        StringParameter sp_startTime;
35        StringParameter sp_now;
36        StringParameter sp_fmt;
37        Parameter p_responsesHandled;
38        Parameter p_ticks {
39            Unit: tick;
40        }
41        Parameter p_remainingSeconds {
42            Unit: s;
43        }
44    }
45}