Lesson 30: Outbound API and JSON

The Shop Owners and Farmers want a communal address book that they can contribute to and will include people likely not on the system so as to easily share contact information with each other and recruit new users.



Lesson Outcomes

By the end of this lesson you should know how to use several forms of the DSL outbound API to perform authorised requests to outside applications including GET, POST, and DELETE.




New & Modified App Files

./model/objects/AddressBookListing.mez

./web-app/lang/en.lang

./web-app/presenters/AddressBookMgmt.mez

./web-app/views/AddressBookMgmt.vxml




Helium 2 Service

In this tutorial we are going to use our own Helium 2 service to act as the outside application which will be contacted using the outbound API. Please ensure you have completed the /wiki/spaces/H2D/pages/4358641 until at least Part 7, and that your service is running as expected before continuing.




AddressBookMgmt

Essentially this will be a CRUD view for objects that are persisted outside of our Helium 1 database and rather in our Helium 2 database using the API services between these two applications to manage these objects. If you have not yet set up your Helium 2 service, please do so before continuing with this tutorial.

The view we are going to create shouldn't look much different from anything we have covered so far:

AddressBookMgmt.vxml
<?xml version="1.0" encoding="UTF-8"?>
<ui xmlns="http://uiprogram.mezzanine.com/View"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://uiprogram.mezzanine.com/View ../View.xsd">
	<view label="view_heading.address_book_management" unit="AddressBookMgmt" init="init">

	<menuitem label="menu_item.address_book" icon="Globe" order="3">
		<userRole>System Admin</userRole>
    	<userRole>Farmer</userRole>
    	<userRole>Shop Owner</userRole>
	</menuitem>

    <textfield label="textfield.search_name">
      <binding variable="searchParam"/>
    </textfield>

    <submit label="submit.search" action="searchAddressBook"/>

    <table title="table_title.found_results">
      <visible variable="showResults"/>
      <collectionSource variable="foundResults"/>
      <column heading="column_heading.name">
        <attributeName>name</attributeName>
      </column>
      <column heading="column_heading.mobile_number">
        <attributeName>mobileNumber</attributeName>
      </column>
      <column heading="column_heading.address">
        <attributeName>address</attributeName>
      </column>
      <column heading="column_heading.age">
        <attributeName>age</attributeName>
      </column>
      <rowAction label="button.update" action="updateListing">
        <binding variable="selectedListing"/>
      </rowAction>
      <rowAction label="button.delete" action="deleteListing">
        <binding variable="selectedListing"/>
      </rowAction>
    </table>

    <textfield label="textfield.name">
      <visible variable="showResults"/>
      <binding variable="name"/>
    </textfield>

    <textfield label="textfield.mobile_number">
      <visible variable="showResults"/>
      <binding variable="mobileNumber"/>
    </textfield>

    <textfield label="textfield.age">
      <visible variable="showResults"/>
      <binding variable="age"/>
    </textfield>

    <textarea label="textarea.farm_address">
      <visible variable="showResults"/>
      <binding variable="address"/>
    </textarea>

    <submit label="submit.add_listing" action="submitListing">
      <visible variable="showResults"/>
    </submit>

	</view>
</ui>

Firstly we allow all the users to see this <menuitem> so as to create a communally contributed address book tool.
<textfield> is then used to take input from the user and perform a search on that input.
The <textfield> displays all of the listings found based on the user's input along with an action="deleteListing" and action="updateListing" <rowAction>.
Lastly the page has several text inputs and a <submit> which will be used to update or create a listing.

We also need a new non-persistent model object to serve as a <collectionSource> for our table and temporarily house the results returned from our search:

AddressBookListing.mez
object AddressBookListing {

    // The id of this record from the remote service
    uuid id;

    // General attributes of the address book listing
    string name;
    string address;
    int age;
    string mobileNumber;
}

You should be familiar with the view components used above as well as how to create non-persistent model objects.

Remember to update the en.lang file with the new translations and see the attached source code for any omitted parts if necessary.




Outbound api:get and AddressBookMgmt

We will now look closer at the functions used in this management view starting with the search function:

presenter snippet (AddressBookMgmt.mez)
void searchAddressBook(){
  if (searchParam != null && searchParam != "") {
    foundResults = null;
    foundResults = getAddressBookListings();
  } else{
    Mez:alertError("alert_error.no_input_given");
  }
}

AddressBookListing[] getAddressBookListings() {

    string baseUrl = "http://127.0.0.1:8090/3ec84978-b8aa-4d03-864e-3c50afebb93a";
    string listingEndpoint = "/1/listing/";
    string url = Strings:concat(baseUrl, listingEndpoint);
    string userName = "user1";
	string encodedParam = String:urlEncode(searchParam);

    AddressBookListing[] result;

    try {
        MezApiRequest request = MezApiRequest:new();
        request.url = url;
        request.credentials = userName;
		request.queryParameters = "{}";
        request.queryParameters.jsonPut("name", encodedParam);

        MezApiResponse response = api:get(request);
        int responseCode = response.code;
        string responseMessage = response.message;
		string requestUrl = response.url;

        if(responseCode < 200 || responseCode >= 300) {
            Mez:alertError("alert_error.listing_get_failure");
            return result;
        }

        result = createAddressBookListingsFromJson(response.body);
        showResults = true;
        return result;
    } catch(ex) {
        string exceptionMessage = ex.message;
        Mez:alertError("alert_error.listing_get_exception");
        return result;
    }
}

The searchAddressBook() function validates that we have received input before calling any API to ensure that we do not needlessly call the service without the correct parameters. The getAddressBookListings() function on line 28 is our first brush with the outbound API.

Helium has two added non-persistent objects used with the outbound API functionality to accomplish these calls. The one, MezApiRequest, holds the values for your request in its attributes such as the url that will be used as the endpoint for the service being contacted, the body of the request if needed, which credentials should be used during the request (more on this later), and finally any headers that might be needed by the service. The second, MezApiResponse, holds values that can be expected in a standard REST API response such as the body of the response, a code that represents specified HTTP response codes, a message describing the HTTP response code if provided, and lastly a boolean success attribute indicating whether the response code is in the 2xx range (succces) or not. These are used along with the following built-in functions that are provided for making outbound API calls. Note the api namespace:

api:getPerforms the outbound call with a GET HTTP method.
api:postPerforms the outbound call with a POST HTTP method.
api:deletePerforms the outbound call with a DELETE HTTP method.
api:putPerforms the outbound call with a PUT HTTP method.

The functions all expect a single argument of type MezApiRequest and return a single instance of MezApiResponse.See more about these objects and functions here.

We have a string value that represents the baseUrl of the endpoint we will be using and we concatenate that with the endpoint for the specific resource we will be using, listingEndpoint. The searchParam is encoded using the built-in string function, String:urlEncode(), and then added to the queryParameters json attribute on the MezApiRequest object. We alse define which credentials we want to use when making this call, although note that these credentials have to be set up for our application. There are two ways to add credentials to your application, use the core Helium API, or use the user interface provided for the application under the app admin page on the Helium core application. You can read more about the credentials management here. Remeber to add your credentials according to what you have defined during your Helium 2 service setup. The user you will use to make this call will have to have at least READER privileges.

Pay attention to how the MezApiRequest is created using the values we have just discussed and then passed as the parameter for the api:get() built-in function call made on line 42. On this same line you can also see how the returned object is assigned to a variable of type MezApiResponse. The response is then used to display a success or error message to the user as feedback on their response. The body of the response now holds the result of our request made to the service in JSON format. In the next section we will look at how we can interpret that JSON body. Also note that the MezApiResponse.url attribute holds the final URL used by Helium when it made the call as the added queryParameters might have changed it.




jsonGet and AddressBookListing

On line 51 in the API function above, the response body is passed to the createAddressBookListingFromJson() function. While the response is a single json object, we know this reponse will be an array of 0 or more elements so we convert it to a jsonarray variable before converting it again to an array of json objects (json[]). Look at how these variables are implicitly cast, simply by assigning the variables to other variables with different types on lines 72 and 73 below. Also note that casting from jsonarray to json[] is a relatively expensive procedure and should be avoided for large arrays.

presenter snippet (AddressBookMgmt.mez)
AddressBookListing createAddressBookListingFromJson(json listingJson) {
    AddressBookListing listing = AddressBookListing:new();
    listing.id = listingJson.jsonGet("id");
    listing.name = listingJson.jsonGet("name");
    listing.address = listingJson.jsonGet("address");
    listing.age = listingJson.jsonGet("age");
    listing.mobileNumber = listingJson.jsonGet("mobileNumber");
    return listing;
}

AddressBookListing[] createAddressBookListingsFromJson(json listingsJson) {
    jsonarray listingsJsonArray = listingsJson;
    json[] listingsJsonCollection = listingsJsonArray;

    AddressBookListing[] result;
    foreach(json jsonListing: listingsJsonCollection) {
        AddressBookListing newListing = createAddressBookListingFromJson(jsonListing);
        result.append(newListing);
    }

    return result;
}

We then just loop over this json[] and call the function above it on each element in the array which creates a new AddressBookListing. The DSL built-in function jsonGet() takes a single string parameter which is used to retrieve and return the value for that specific key in the json object that it is called on. In the example above we can see how it is used to retrieve the attributes for the listing and then assign them to the newly created AddressBookListing. Looping over the array in the response body creates our collection source for our table.




Outbound api:post and jsonPut

We can now take any input given to the application by the user and create/update a listing using an outbound API post call.

presenter snippet (AddressBookMgmt.mez)
void submitListing(){
  string baseUrl = "http://127.0.0.1:8090/3ec84978-b8aa-4d03-864e-3c50afebb93a";
  string listingEndpoint = "/1/listing";
  string url = Strings:concat(baseUrl, listingEndpoint);
  string userName = "user2";
  json body = "{}";
  uuid id;

  if (selectedListing == null) {
    AddressBookListing obj = AddressBookListing:new();
    id = obj._id;
  } else {
    id = selectedListing.id;
  }

  body.jsonPut("id", id);
  body.jsonPut("name", name);
  body.jsonPut("address", address);
  body.jsonPut("age", age);
  body.jsonPut("mobileNumber", mobileNumber);

  try {
      MezApiRequest request = MezApiRequest:new();
      request.url = url;
      request.credentials = userName;
      request.body = body;

      MezApiResponse response = api:post(request);
      int responseCode = response.code;
      string responseMessage = response.message;

      if(responseCode < 200 || responseCode >= 300) {
          Mez:alertError("alert_error.listing_get_failure");
      } else {
        searchAddressBook();
        Mez:alert("alert.uploaded_data_saved");
      }
  } catch(ex) {
      string exceptionMessage = ex.message;
      Mez:alertError("alert_error.listing_get_exception");
  }
}

The API request is built in mostly the same fashion as before with slightly different endpoints, however, we are also now using a different set of credentials that has WRITER privileges and we will add a body to our request where there was no body before. The body attribute is of json type and will contain the attributes for our listing as expected by the service we are calling. The jsonPut() built-in function is used to add fields and values to a json variable which are passed to the function as parameters. On lines 110-114 we can see how the values received from user input with their respective fields are being added to the json body of our request. This is then added to our MezApiRequest object which will be used with the api:post() built-in function this time.

Note that the action="updateListing" <rowAction> only takes the listing selected and populates the input fields used to create a listing. When submitting these values then the lines 102-107 evaluates whether it should create a new uuid which the service will use to create a new listing, or whether it should use the selected listing's uuid indicating to the service that this listing should be updated with the values in the body.




Outbound api:delete

We also have the api:delete() built-in function that is called similarly to all of the previous methods only updated to the corresponding REST resource it will utilise and using the credentials of a user that has WRITER privileges.

presenter snippet (AddressBookMgmt.mez)
void deleteListing(){
  if (selectedListing != null) {
    string baseUrl = "http://127.0.0.1:8090/3ec84978-b8aa-4d03-864e-3c50afebb93a";
    string listingEndpoint = "/1/listing/";
    string url = Strings:concat(baseUrl, listingEndpoint, selectedListing.id);
    string userName = "user2";
    try {
        MezApiRequest request = MezApiRequest:new();
        request.url = url;
        request.credentials = userName;

        MezApiResponse response = api:delete(request);
        int responseCode = response.code;
        string responseMessage = response.message;

        if(responseCode < 200 || responseCode >= 300) {
            Mez:alertError("alert_error.listing_get_failure");
        } else {
          searchAddressBook();
          selectedListing = null;
        }
    } catch(ex) {
        string exceptionMessage = ex.message;
        Mez:alertError("alert_error.listing_get_exception");
    }
  }
}




Further Reading

There are several more advanced concepts regarding both the Outbound API and Helium JSON handling that have not been covered here. It is recommended users read up more about these topics here:

Outbound API

Native JSON Types




Important Note

While most operations in Helium occur within a transaction that will be rolled back if any exceptions occur, Outbound API calls are executed synchronously. This means that once the call is made, it cannot be rolled back or reverted. Take note of this when using these Outbound API calls within other Helium operations such as scheduled functions as this mismatching of behaviour could cause unexpected results.

Marking which outbound calls were successful or not would also not be possible in this case as this would likely be an operation attempting to persist a value to the database and would therefore also roll back along with the failed transaction.




Lesson Source Code

lesson_30.zip