Contents


Use Clojure to write OpenWhisk actions, Part 2

Connect your Clojure OpenWhisk actions into useful sequences

Learn how by developing an inventory control system

Comments

Content series:

This content is part # of # in the series: Use Clojure to write OpenWhisk actions, Part 2

Stay tuned for additional content in this series.

This content is part of the series:Use Clojure to write OpenWhisk actions, Part 2

Stay tuned for additional content in this series.

This is the second in a series of three tutorials that illustrate Clojure and OpenWhisk through the development of an inventory control system. In Part 1, I explained how to use Clojure (a functional programming language that's based on Lisp) to write OpenWhisk actions using the Node.js runtime and the ClojureScript package. Here in Part 2, I show you how to use OpenWhisk sequences to combine actions into useful chunks that do the work of an application. Part 3 will show you how to interface with an external database, and how to use logging to debug your own Clojure in OpenWhisk applications.

By the end of Part 2, you will have most of the functionality of the sample application, the inventory management system. You will be able to use Clojure actions to produce HTML to display information and to use information from HTML forms to modify the information stored in the application.

What you'll need to build your application

This tutorial builds upon information from the first tutorial in this series: "Use Clojure to write OpenWhisk actions, Part 1: Write clear, concise code for OpenWhisk using this Lisp dialect." In addition, you will need:

  • A basic knowledge of OpenWhisk and JavaScript (Clojure is optional; this tutorial explains what you need when you need it)
  • A free Bluemix account (Sign up here)

Run the appGet the code

From JSON to HTML

Normally, OpenWhisk actions communicate using JSON. However, browsers expect HTML. So we need to find a way to turn JSON into HTML, and do it in a modular fashion using Clojure.

In this tutorial, you progress from a single action in Clojure and OpenWhisk to sequences that actually communicate with the browser.

Putting item data into a table

The first step is to put the item data into a table. To do that, create a new action in a new directory. Use the same package.json and main.js files that were created in Part 1. The only file that is different this time is action.cljs:

(ns action.core (:require clojure.string))



(defn cljsMain [params] (
    let [
      cljParams (js->clj params)
      data (get cljParams "data")
      dataKeys (keys data)
      rowsAsList (map #(clojure.string/join ["<tr><td>" %
                                    "</td><td>" (get data %) "</td></tr>"])
                      dataKeys)
      rowsAsString (clojure.string/join rowsAsList)

    ]

    {"html" (clojure.string/join
        ["<table><tr><th>Item</th><th>Amt. in Stock</th></tr>"
        rowsAsString
        "</table>"
        ])
    }
  )
)

In the action.cljs listing above, the first name declares the name space, as before. But ns has an additional parameter: (:require clojure.string). This parameter imports the clojure.string library.

The cljsMain function is written in a similar style to the one in Part 1. A number of intermediate values are calculated in a let function and then the final value is created:

(defn cljsMain [params] (
    let [
      cljParams (js->clj params)
      data (get cljParams "data")

The keys function takes a hash table and returns a list of the keys. In this case, the data is the output of the database action, so the keys are item names:

      dataKeys (keys data)

The next line uses one of the most important functions in functional programming, map:

      rowsAsList (map #(clojure.string/join ["<tr><td>" %
                                    "</td><td>" (get data %) "</td></tr>"])
                      dataKeys)

The map function takes a function and a list, and returns a list of the results of running the function on each item in the list.

To see the map function in action on the REPL website, run (map #(* 2 %) '(1 2 3 4)). (For more information about REPL, see Part 1.) The single quote (') signifies that this is just a list, not a function call.

Results of running map
Results of running map

The function in the case of action.cljs is clojure.string/join. This function receives a vector of strings, and returns a string that joins all of them. In this case, it is a table row with two columns: the item's name and the amount.

The result of map is a list. However, you want to produce a single value, a string. To do that, you join together all the values. This gives you a string with all the database rows:

      rowsAsString (clojure.string/join rowsAsList)

    ]

Finally, add the table tags and the header row:

    {"html" (clojure.string/join
        ["<table><tr><th>Item</th><th>Amt. in Stock</th></tr>"
        rowsAsString
        "</table>"
        ])
    }
  )
)

Note: To send actions to OpenWhisk, it is useful to have a script similar to the one below. It performs all the steps, including the deletion of the action if it already exists. This is for debugging purposes; you often need to try multiple potential solutions before you find what works.

#! /bin/sh
# Send the action to OpenWhisk

npm install
zip -r action.zip package.json main.js action.cljs node_modules
wsk action delete inventory_json2html
wsk action create inventory_json2html action.zip --kind nodejs:6

Putting actions in a sequence

The next step is to combine the two actions you have—the mock database and the translation into an HTML table—into a sequence. To do this, perform these steps:

  1. Go to the OpenWhisk console in Bluemix.
  2. Click Develop on the sidebar and then the action inventory_dbase.
  3. Click Link into a Sequence.
  4. Click the MY ACTIONS tile.
  5. Select inventory_json2html and click + Add to Sequence. This will create a two-action sequence.
  6. Then click → This Looks Good.
  7. Name the sequence "ShowItems" and then click Save Action Sequence.
  8. Click ShowItems and then Run this Sequence using the following parameter:
    {
     "action": "getAll"
    }
  9. Confirm that you get an HTML value with a table in the response.

Create a full HTML file

So now you can produce a table. In a real application, people expect a lot more than that. For example, they expect links to show them other actions they can take. To provide those links, you create another action (call it "inventory_table2html"). As before, the only file that is different is action.cljs.

(ns action.core (:require clojure.string))

(def header (js* "require('fs').readFileSync(__dirname + '/../../../header')"))
(def footer (js* "require('fs').readFileSync(__dirname + '/../../../footer')"))


(defn cljsMain [params] (
    let [
      cljParams (js->clj params)
      htmlTable (get cljParams "html")
      bootstrapTable (clojure.string/replace htmlTable
          "<table>"
          "<table class=\"table table-striped\"> ")
      delme (prn "Parameter HTML:")
      delme (prn htmlTable)
    ]

    {"html" (clojure.string/join [header (clojure.string/replace bootstrapTable "$$$" "\"") footer])}
  )
)

I could have specified the HTML code to come before and after the table in string constants. However, that would require me to escape the double quote ("), which is very common in HTML. Instead, it is easier to create two files, called header and footer, for the HTML that comes before and after the table. As long as they are added to the action.zip file, they will be in the same directory as the action.cljs file and it will be able to read them.

However, if you try to use just the file name __dirname + "/header" (in JavaScript code, as explained below), you get an error. When the Clojure file is evaluated, the system is actually three directories deeper, as you can see from this log snippet:

Screen capture showing log
Screen capture showing log

This error is returned from the system call (call to the operating system kernel) open. The error code, ENOENT, stands for "Error: NO ENTry" because the file does not exist in the directory. The file name two lines after the error code shows us the directory.

There are several items that are new in this action.cljs file:

  • The following two lines read the header and footer files. Instead of translating the JavaScript code to read a file to Clojure, I decided to simply use js*. For a library function call, I think this is clearer.
    (def header (js* "require('fs').readFileSync(__dirname + '/../../../header')"))
    (def footer (js* "require('fs').readFileSync(__dirname + '/../../../footer')"))
  • The table we get from the previous action is a standard HTML one. However, the complete web page uses the Bootstrap template, which requires that classes be added to the table tag. The solution is the clojure.string/replace function, which can replace one string with another (it could also use regular expressions):
          bootstrapTable (clojure.string/replace htmlTable
              "<table>"
              "<table class=\"table table-striped\"> ")
  • For debugging purposes, it is useful to produce log entries. The function that writes to standard output is prn. But to use any function inside the let, you need to assign the result somewhere. Here is the code that uses it:
    delme (prn "Parameter HTML:")
    delme (prn h tmlTable)

    To see the messages, click Show Logs in the OpenWhisk console. You should then see a page that looks like this:

    Screen capture showing logs
    Screen capture showing logs

Serve the HTML to the Internet

Now you can produce HTML. However, it is buried inside a JSON structure. You could write an index.html that reads it, as I explain in another tutorial, "Build a user-facing OpenWhisk application with Bluemix and Node.js." But it is also possible to have the sequence served directly to the browser:

  1. Return to OpenWhisk.
  2. Click Develop and then the ShowItems sequence.
  3. Click Extend and select MY ACTIONS > inventory_table2html.
  4. Click + Add to Sequence.
  5. Click Save Your Changes.
  6. Click APIs on the left sidebar.
  7. Click Create Managed API +.
  8. Create an API with these parameters:
    API name Clojure Inventory
    Base path for API /clj_inventory
    CORS Selected (enable CORS)
  9. Create an operation within the API with these parameters:
    Path /showItems
    Verb GET/clj_inventory
    Package containing action Default
    Action ShowItems
    Response content type text/html
  10. Click Save & expose.
  11. Click API Explorer on the left sidebar, click the one action in the API (getShowitems), and copy the URL from the curl command.
  12. Paste the URL into a browser, and follow it with
    ?action=getAll.
    Confirm that you get a table with all the items.
  13. Replace action=getAll with action=getAvailable.
    Confirm that you get a table with only those items that are in stock.

Getting browser input into a sequence

In the previous section, I used a URL query parameter to send information to an OpenWhisk action. This works well when you have one simple parameter, such as action. However, when sending a lot of information that method looks ugly. It is better to use POST and send the information without showing it to the user.

There are two ways to accomplish this: You could use JavaScript in the browser (preferably using a library such as Angular) to create JSON to post, or you could create a normal HTTP form and do the transformation in OpenWhisk. For the purpose of learning Clojure on OpenWhisk, the second method is clearly better.

The applications that actually modify the data are composed of two sequences. The first creates a form with all the available items (and the amount in stock), and the second processes the form results. This tutorial explains the point-of-sale application. The other two applications (reorder and correction) are nearly identical.

Create the form

To create the form, create a new action called inventory_purchaseForm (see the source code). This action is very similar to inventory_json2HTML, except that the table is embedded in a form, and there is a separate column for the amount purchased.

The one problem is that a form requires the use of double quotes ("). Unfortunately, because of the way clojure.string/replace operates, having this character in the string, whether it is escaped or not, confuses the inventory_table2HTML action. There may be a more elegant solution, but in the interest of time, I just use three dollar signs ($$$) instead, and then replace them just before sending the response out of inventory_table2HTML, in this line:

{"html" (clojure.string/join [header (clojure.string/replace bootstrapTable "$$$" "\"") footer])}

Next, create a sequence called PurchaseForm. This sequence is similar to ShowItems, except that the middle action is inventory_purchaseForm. Then add it to the API with the path /purchase, the verb GET, and the reply content type text/html.

Process the submitted form

To see what exactly gets submitted, and how it is available, I created an echo action and configured it as the reply to a POST verb for /purchase. This is the response:

{
  "T-shirt L": "1",
  "__ow_method": "post",
  "__ow_headers": {
      ...
  },
  "__ow_path": "",
  "T-shirt S": "",
  "T-shirt XL": "3",
  "action": "getAvailable"
}

Now you get the parameters from the purchase form, but also action (because we didn't change the URL, which includes the query) and three internal parameters you don't need: __ow_method, __ow_headers, and __ow_path. Also, if any rows are unfilled, you will have an empty string.

To turn this form into JSON that the inventory_dbase action understands, create a new action called inventory_processPurchase. You can see the source code here. Most of the code is similar to what we have done before, but there are a couple of differences.

  • To remove the keys that do not belong in the data, I use filter. However, the condition is a bit more complicated. It uses the or function with five different conditions.
    	realKeys (filter #(not (or (= % "action")
    				     (= % "__ow_method")
                                       (= % "__ow_headers")
                                       (= % "__ow_path")
    				     (= (get usefulParams %) "")
                               )))

    In Clojure (and other LISP variants), most calculation functions can take multiple values and handle them. To see this in action on the REPL website, run (* 2 3 4 5 6 7):Screen capture showing results of         command
    Screen capture showing results of command

    As you can see, all the numbers are multiplied together to reach 5040.
  • Another difference is the use of reduce to create the data hash table. This function has a function as its first parameter and then a collection (list, vector, and so on). It turns that collection into a single value by running the function on the first two values, then on the result and the third value, and so on. In mathematical terms, it looks like f(f(f(v1,v2),v3),v4). To see this in action on the REPL website, run (reduce #(+ %1 %2) '(1 2 3 4 5 6 7)): Screen capture showing results of         command
    Screen capture showing results of command

    The numbers are added together and you get 28.

To create the hash table, you use the assoc function which accepts a hash table as its first parameter. This means that the list needs to start with an empty hash table. To get that, you use the list* function that takes arguments and puts them in the beginning of the list it receives as its last argument.

data (reduce #(assoc %1 %2 (get usefulParams %2)) (list* {} realKeys))

To see this in action on the REPL website, run (assoc {} :a :b). You get a hash table with one item whose key is :a and whose value is :b.

Then run (list* 1 2 3 4 5 6 '(7 8)) and see that the result is a list of numbers 1-8:

Screen capture showing results of         command
Screen capture showing results of command

Next, create the sequence. Here are the actions and the reasons we have them:

ActionPurpose
inventory_processPurchase Create the data and action for the database
inventory_dbase Actually process the data, returning the item list
inventory_json2html Convert the item list into an HTML table
inventory_table2html Convert the HTML table into a full web page

Finally, create an API operation for the path /purchase, the verb POST, and the reply content type text/html to call the sequence. This allows you to process the result.

The other two pages, which allow the user to either add to the existing inventory or set the amount in the database, are nearly identical. The differences are in:

  • The query used to start them (action=getAll instead of action=getAvailable)
  • The value of the action parameter after processing the form
  • The path for the API

Also, when processing a reorder, it is necessary to turn the values into strings. Otherwise, the plus function "sees" two strings, and it is really the JavaScript plus, so it concatenates them. The best place to do this is the database action, using the cljs.reader/parse-int function.

You can read the code in the repository and see for yourself if you are interested. Note that there is no need for a new sequence to show the form, and the sequence to process it uses only one different action to support having different commands to the database. There are ways to use the same action, but they'd add needless complication.

Conclusion

By following this tutorial, you have progressed from a single action in Clojure and OpenWhisk to sequences that actually communicate with the browser. The application is now complete, except for being completely useless. The information is stored in a "database" which is a variable that gets deleted every time OpenWhisk decides that the database action has not been used in a while and the memory it occupies would be better used for another purpose. In the final tutorial in the series, I show you how to use a more permanent storage option, and I introduce you to some other odds and ends.


Downloadable resources


Related topics


Comments

Sign in or register to add and subscribe to comments.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Cloud computing
ArticleID=1049869
ArticleTitle=Use Clojure to write OpenWhisk actions, Part 2: Connect your Clojure OpenWhisk actions into useful sequences
publish-date=09192017