Modules and External APIs Lesson

Learning Objective

After completing this lesson, you will be able to:

  • use modules in KRL

  • wrap an external API call and make it available from a module

Motivation

A module is defined on Google as "any of a number of distinct but interrelated units from which a program may be built up." Some classic examples of modules you may have worked with are program libraries or packages. If you've ever seen an "import" or "include" at the top of a file, you are taking advantage of some module. In KRL, the keyword is use module.

In KRL, there are three primary use cases for using modules:

First, it is pretty easy to imagine a case where you want to use some microservice/external API in your application, but you don't necessarily want to write all these API calls yourself. In fact, Twilio and other API services already provide API wrappers in the popular languages such as Python, Java, C++ etc. In this lesson, we will create our own wrapper ruleset which wraps API calls to a movie database service. Once we have this ruleset, we can use it as a module in our main applications to do some really cool stuff.

Second, as applications get bigger, code needs to be encapsulated into smaller, logically related rulesets. One ruleset/component of this once monolithic app may need access to the functions and actions defined in a different ruleset. Instead of writing code to make the pico query itself (something you should never do), you can instead "use" the needed ruleset as a module and just call the function directly.

Third, you may wish to extend a ruleset to add functionality, or as part of an application not foreseen by the author of the original ruleset. You may access components of the original ruleset by using it as a module.

Prerequisites

You should have done the following before starting this lesson:

Contents

Modules

Modules make KRL programming easier. KRL has a powerful parameterized module facility. 

In this lesson, we're going to explore how modules can be used to wrap an external API and make it available inside a pico. Along the way we'll explore using HTTP actions, creating our own actions, and managing API keys. 

For this lesson, we're going to be using the The Movie Database API to rate movies. They provides a RESTful API for both obtaining lists of movies and providing ratings. Before you can complete this lesson, you'll need to register for an account. 

Overview

In this example, we are going to write two rulesets.

  1. An API wrapper, such as might be provided by the API (if they provided one for KRL)

  2. Our application, which will use the above ruleset as a module

Note on ruleset identifiers

A ruleset can be named or identified as you like. We have seen hello_world used as a ruleset identifier (RID) in previous lessons. Every pico created has a ruleset named io.picolabs.pico-engine-ui and you'll notice that it has as a prefix our domain name, but reversed. This is similar to the convention used to name Java classes.

In this lesson, we are going to pretend that The Movie Database is allowing us to use their domain name, because the ruleset/module that we will create ought to be provided by them. Perhaps at some point we could offer it to them. In this lesson we are going to name our sample application simply my_movie_app. If we were expecting to commercialize our application, we would obtain a domain name and use that.

The following screenshot shows the two rulesets installed in a pico.

Notice that when we install our application ruleset in the pico, we configure it with our api_key and session_id so that our application can let the module know what these values are.

Handling secrets

The secrets for this API consist of api_key and session_id but this will differ from API to API. It is important not to make these public. I keep mine in a text file associated with a project, but not a part of the folders that are under source code control. Generally, I have the last line of that file be the secrets in a map, ready to configure when I install my ruleset into a pico. I use this command to get the config value into the copy buffer (on a Mac):

1 $ tail -1 secrets | pbcopy

Then paste into the Config block when installing the ruleset.

Here is the part of the module code which retrieves the secrets:

1 2 3 4 5 6 7 8 9 ruleset org.themoviedb.sdk { meta { ... configure using apiKey = "" sessionID = "" ... } }

The names apiKey and sessionID are globally available. They default to the empty string. The code in this ruleset would not operate correctly with these default values, so the application must supply them.

An application using this ruleset as a module begins like this:

1 2 3 4 5 6 7 8 9 ruleset my_movie_app { meta { use module org.themoviedb.sdk alias sdk with apiKey = meta:rulesetConfig{"api_key"} sessionID = meta:rulesetConfig{"session_id"} ... } }

Notice that we understand that the API module we are using in my_movie_app needs these values, and so we supply them using the with clause of the use module statement. This code assumes that our ruleset will be configured with values named api_key and session_id when it is installed in a pico. Documentation we provide with our ruleset would include instructions for doing this.

Actions and Functions

Unlike many programming languages, KRL rules make a syntactic distinction between operations that merely read state and those that change it. The KRL prelude is expected to not have an effect on state, either internal or external to the ruleset. This is more than stylistic; there are security and programming advantages to this segregation. Specifically, the action (and postlude) are conditional while the prelude declarations are not. Consequently, state changes made in the prelude that affect state are difficult to guard.

KRL itself takes pains to not permit state changing language features in the prelude, but a module developer could create functions that make state changes in the external API. In general, you should avoid this so that your module is easy for developers to use correctly and easily. In KRL rules, actions should generally be used for API requests done with a POST, PUT, or DEL since those requests change state in the API while GET requests should be used in functions. This ensures that operations in your API that affect external state can be guarded by the rule's conditional and any calls made in the prelude or event expression will not change state. Of course, how your module does this will depend a lot on how the API is built. 

A function in an API module

We will define one function in the API module. A complete API module would define one function for each GET operation the API supports.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ruleset org.themoviedb.sdk { meta { ... provides getPopular ... } global { base_url = "https://api.themoviedb.org/3" getPopular = function() { queryString = {"api_key":apiKey} response = http:get(<<#{base_url}/movie/popular>>, qs=queryString) response{"content"}.decode() } ... } }

This ruleset (which would logically be supplied by the API, but we are writing it here) provides functions which a ruleset using it can call. The implementation simply does an HTTP GET operation and returns the resulting content.

Using an API function

The ruleset which will use the API module looks like this (with respect to the provided function):

1 2 3 4 5 6 7 8 9 10 11 12 13 14 ruleset my_movie_app { meta { use module org.themoviedb.sdk alias sdk with ... shares getPopular ... } global { getPopular = function() { sdk:getPopular() } ... } ... }

We specify the module to be used in line 3, giving it the alias (for use within this ruleset) of sdk and supplying the needed secrets as discussed above.

In line 5 we share a function that we have named getPopular for use in a browser or the Testing tab or a single page application. That function is defined in lines 8-10 and is a simple wrapper around the function provided by the API module. In this case the name of the two functions happens to be the same, but that is not necessarily so.

An action in an API module

We will implement one action in this sample API module. Again, a complete API module would define an action for every HTTP POST which the API defines.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ruleset org.themoviedb.sdk { meta { ... provides getPopular, rateMovie } global { ... rateMovie = defaction(movieID, rating) { queryString = {"api_key":apiKey,"session_id":sessionID} body = {"value":rating} http:post(<<#{base_url}/movie/#{movieID}/rating>> ,qs=queryString,json=body) setting(response) return response } } }

The action is defined using the defaction keyword. See the "User-Defined Actions" page. Noticed that it is provided (to dependent rulesets) by this API module in line 4, and it is defined in lines 8-14. It sets up some bound values (lines 9-10) then takes an action, an HTTP POST (lines 11-12) and binds the HTTP response to the name response, which it then returns as the result of the action.

Using an API action

Our application ruleset can use an API module action wherever an action is allowed/required in KRL. Here we use it inside of a rule:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 ruleset my_movie_app { meta { use module org.themoviedb.sdk alias sdk with ... ... } ... rule rate_movie { select when movie new_rating movieID re#(.+)# rating re#^(\d+([.]\d+)?)$# setting(movieID,rating) pre { rating_value = rating.as("Number") valid_rating = 0.5 <= rating_value && rating_value <= 10 } if valid_rating then sdk:rateMovie(movieID,rating) setting(response) fired { ent:lastResponse := response ent:lastTimestamp := time:now() raise movie event "rated" attributes event:attrs } } }

Notice again that the alias sdk has been established (line 3) for the API module we are using. This alias is used in line 17.

Our rule, which selects on the movie:new_rating event, expects a non-empty attribute named movieID and a numeric attribute named rating. These are checked for in lines 10 and 11, respectively, and bound to internal (to the rule) names which happen to match the attribute names in line 12. We convert one of the attributes from the passed-in String to a Number in line 14, using the universal as operator. We check that the rating is in the expected range in line 15. If the rating is value (line 17) then we take the HTTP POST action in line 17, also capturing the HTTP response, which we then save in the pico in line 19 (along with a timestamp in line 20).

Finally, in line 21 we raise a movie:rated event for future use, if any. This follows the principle of avoiding dead-end rules.

Note that the rate_movie rule uses an event expression to capture the values from the movieID and rating event attributes and assign those to variables. The regular expressions require that those be non-empty, so this rule won’t be selected if they are.

You can alternately use event:attr() in the pre block to save these values.

pre {

movieID = event:attr(”movieID”);

}

Using an application

Our ruleset could be used as the back-end for a web-based single page application, or by some other ruleset. For now, you can just test it using the Testing tab:

An astute reader will have noticed that there must be another shared function, named lastResponse, whose code we haven't yet included. Here it is:

1 2 3 4 5 6 7 8 9 10 11 12 13 ruleset my_movie_app { meta { ... shares getPopular, lastResponse } global { ... lastResponse = function() { {}.put(ent:lastTimestamp,ent:lastResponse) } } ... }

This function (lines 8-10) simply allows us the see the latest HTTP response, which it packages up (line 9) into a Map, using the Map put Operator.

The two rulesets described above can be found in the pico_lessons GitHub repo.

Conclusion

We now have a module that wraps the The Movie Database API in functions and actions that are more convenient for KRL rulesets to use. We've also taken steps to protect sensitive keys while making their use convenient.

As always, it is a good idea to test your code during development.

Exercise

  1. Sign up for a developer account at Twilio

  2. Study the Twilio API documentation for sending an SMS and making Twilio requests.

  3. Generate an account_sid and the corresponding auth_token

  4. Create a local file holding these keys. Do not check it in to any public repository.

  5. Create a Twilio module to wrap their API:

    1. Write a function to return a page of messages sent

    2. Write a user-defined action to send an SMS

  6. Write another ruleset to use and test your Twilio module by:

    1. calling the function and returning the messages

    2. defining a rule which actually sends an SMS