目次


Clojure を使用して OpenWhisk のアクションを作成する, 第 2 回

Clojure OpenWhisk の複数のアクションを結合して有用なシーケンスにする

インベントリー管理システムの開発方法を学ぶ

Comments

コンテンツシリーズ

このコンテンツは全#シリーズのパート#です: Clojure を使用して OpenWhisk のアクションを作成する, 第 2 回

このシリーズの続きに乞うご期待。

このコンテンツはシリーズの一部分です:Clojure を使用して OpenWhisk のアクションを作成する, 第 2 回

このシリーズの続きに乞うご期待。

このチュートリアルは、インベントリー管理システムの開発を通して Clojure と OpenWhisk について説明する、全 3 回からなるシリーズの第 2 回です。第 1 回では、Clojure (Lisp をベースとする関数型プログラミング言語) で Node.js ランタイムと ClojureScript パッケージを使って OpenWhisk のアクションを作成する方法を説明しました。今回は、OpenWhisk のシーケンスを使用して複数のアクションを有用な塊に結合し、アプリケーションの実際の処理が行われるようにする方法を説明します。第 3 回では、外部データベースと連動する方法、OpenWhisk アプリケーション内でロギングを使って独自の Clojure をデバッグする方法を説明します。

この第 2 回で説明する手順を終える頃までには、サンプル・アプリケーションとして使用しているインベントリー管理システムの機能のほとんどが完成して、Clojure アクションによって HTML を生成して情報を表示したり、アプリケーション内に保管されている情報を HTML フォームからの情報で変更したりできるようになります。

アプリケーションを作成するために必要なもの

このチュートリアルは、シリーズ第 1 回のチュートリアル「Clojure を使用して OpenWhisk のアクションを作成する, 第 1 回: Lisp ダイアレクトを使用して OpenWhisk のわかりやすく簡潔なコードを作成する」で説明した内容に基づいています。今回はさらに、以下のものが必要になります。

  • OpenWhisk と JavaScript の基礎知識 (Clojure の知識は必須ではありません。このチュートリアルで、必要なときに必要な部分を説明します)
  • IBM Cloud アカウント (このリンク先のページで登録できます)

アプリを実行するコードを入手する

JSON から HTML への変換

通常、OpenWhisk のアクションでは JSON を使用して情報を受け渡します。けれども、ブラウザーが期待するのは HTML です。したがって、JSON を HTML に変換する方法、しかも Clojure を使ってモジュール式で変換する方法を見つけなければなりません。

このチュートリアルでは、Clojure を使用して作成する OpenWhisk のアクションを単独のものからシーケンスへと進化させて、実際にブラウザーとやりとりできるようにします

品目データをテーブルに取り込む

最初のステップとして、品目データをテーブルに取り込む必要があります。それには、新しいディレクトリーを作成して、そのディレクトリー内に新しいアクションを作成します。使用する package.json ファイルと main.js ファイルは、第 1 回で作成したのと同じものです。以下に示すように、唯一、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>"
        ])
    }
  )
)

上記の action.cljs ファイル内では、前と同じようにファースト・ネームで名前空間を宣言していますが、ns(:require clojure.string) というパラメーターが追加されています。このパラメーターは clojure.string ライブラリーをインポートするためのものです。

cljsMain 関数は、第 1 回で作成したときと同様のスタイルで作成されています。つまり、let 関数内でいくつかの中間値が計算されてから、最終的な値が出されます。

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

keys 関数はハッシュ・テーブルを取り、キーのリストを返します。この例でのデータは、データベースに対するアクションの出力です。したがって、キーは品目名となります。

      dataKeys (keys data)

次の行では、関数型プログラミングにおいて最も重要な関数の 1 つ、map を使用しています。

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

map 関数はパラメーターとして関数とリストを取り、その関数をリスト内のアイテムのそれぞれに対して実行した結果のリストを返します。

map 関数の実際の動作を確かめるには、このリンク先の REPL Web サイト(map #(* 2 %) '(1 2 3 4)) を実行してください (REPL の詳細については、第 1 回を参照してください)。単一引用符 (') は、このコードが関数呼び出しではなく、単なるリストであることを示すためのものです。

map の実行結果
map の実行結果

action.cljs の場合、パラメーターとして取る関数は clojure.string/join です。この関数はストリングのベクトルを受け取り、それらのストリングのすべてを結合したストリングを返します。この例で返されるテーブル行には、2 つの列 (品目の名前と数量) があります。

map の結果はリストですが、この例では単一の値として、1 つのストリングを生成する必要があります。そのためには、すべての値を結合して、データベースのすべての行からなるストリングを取得します。

      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>"
        ])
    }
  )
)

: アクションを OpenWhisk に送信するには、以下に記載するような内容のスクリプトを使用すると便利です。このスクリプトはすべてのステップを実行します。これには、同じアクションがすでに存在する場合はアクションを削除するというステップも含まれます。このスクリプトはデバッグを目的としています。有効なソリューションを見つけるまでに、複数のソリューション候補を試さなければならない場合はよくあります。

#! /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

1 つのシーケンスにアクションを結合する

次のステップは、作成した 2 つのアクション (模擬のデータベースと HTML テーブルへの変換) を 1 つのシーケンスに結合することです。それには、以下の手順に従います。

  1. IBM Cloud 内の OpenWhisk コンソールを表示します。
  2. サイドバー上にある「Develop (開発)」をクリックして、「inventory_dbase」アクションをクリックします。
  3. 「Link into a Sequence (シーケンスにリンク)」をクリックします。
  4. 「MY ACTIONS (マイ・アクション)」タイルをクリックします。
  5. 「inventory_json2html」を選択してから、「+ Add to Sequence (シーケンスに追加)」をクリックします。これによって、2 つのアクションからなるシーケンスが作成されます。
  6. 「→ This Looks Good (このシーケンスを受け入れる)」をクリックします。
  7. 作成されたシーケンスに「ShowItems」という名前を付けてから、「Save Action Sequence (アクション・シーケンスの保存)」をクリックします。
  8. 「ShowItems」をクリックし、以下のパラメーターを指定してから「Run this Sequence (このシーケンスを実行)」をクリックします。
    {
     "action": "getAll"
    }
  9. レスポンスに含めて、テーブルを記述した HTML 値が返されることを確認します。

完全な HTML ファイルを作成する

ここからは、テーブルを生成する方法を説明します。実際のアプリケーションにユーザーが期待する機能はたくさんあります。その一例は、他に実行できるアクションを表示するためのリンクです。そのようなリンクを提供するには、別のアクション (「inventory_table2html」という名前にします) を作成する必要があります。前と同じく、唯一異なるファイルは 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])}
  )
)

テーブルの前後にある HTML コードをストリング定数に格納して指定することもできますが、その場合、HTML コード内に散在する二重引用符 (") をエスケープしなければなりません。それよりも、テーブル前後の HTML として、2 つのファイル (header ファイルと footer ファイル) を作成するほうが、作業が楽になります。この 2 つのファイルが action.zip ファイルに追加されている限り、action.cljs ファイルと同じディレクトリー内に置かれるので、これらのファイルを読み取ることができます。

ただし、(以下に説明するように javaScript コード内で) ファイル名 __dirname + "/header" のまま使おうとすると、エラーを受け取ることになります。以下のログ・スニペットに示されているように、Clojure ファイルが評価されるときに実際にシステムが存在する場所は、ディレクトリー階層の 3 レベル深いところであるためです。

ログのスクリーン・キャプチャー
ログのスクリーン・キャプチャー

上記のエラーは、open システム・コール (オペレーティング・システムのカーネルに対する呼び出し) から返されます。エラー・コード ENOENT は「Error: NO ENTry」を表します。該当するファイルが所定のディレクトリーに存在しないため、エントリーがないと通知しています。エラー・コードの 2 行後に続くファイル名に、その所定のディレクトリーが示されています。

この action.cljs ファイルには、いくつか目新しい点があるので説明しておきます。

  • 以下の 2 行で header ファイルと footer ファイルを読み取ります。ここでは、ファイルを読み取る JavaScript コードを Clojure に変換するのではなく、js* をそのまま使うことにしました。ライブラリー関数の呼び出しには、このほうが簡潔なコードになると思います。
    (def header (js* "require('fs').readFileSync(__dirname + '/../../../header')"))
    (def footer (js* "require('fs').readFileSync(__dirname + '/../../../footer')"))
  • 前のアクションから返されるテーブルは、標準的な HTML テーブルです。けれども、完成した Web ページでは Bootstrap テンプレートを使用するので、テーブル・タグにクラスが追加されていなければなりません。そのためのソリューションは、clojure.string/replace 関数です。この関数は、あるストリングを別のストリングで置き換えることができます (ストリングではなく正規表現を使用することもできます)。
          bootstrapTable (clojure.string/replace htmlTable
              "<table>"
              "<table class=\"table table-striped\"> ")
  • デバッグのために、ログ・エントリーを生成しておくと役立ちます。標準出力に書き込むための関数は prn ですが、let 関数内で何らかの関数を使用する場合は、その結果を割り当てる場所が必要です。そのためのコードは以下のようになります。
    delme (prn "Parameter HTML:")
    delme (prn h tmlTable)

    メッセージを表示するには、OpenWhisk コンソールで「Show Logs (ログの表示)」をクリックします。以下に示すようなページが表示されるはずです。

    ログを表示する画面のスクリーン・キャプチャー
    ログを表示する画面のスクリーン・キャプチャー

HTML をインターネットに配信する

HTML を生成する準備は整いましたが、HTML は JSON 構造の奥に埋め込まれています。別のチュートリアル「IBM Cloud と Node.js を使用してユーザー向け OpenWhisk アプリケーションを作成する」で説明したように、HTML を JSON 構造から読み取る index.html を作成することもできますが、アクション・シーケンスを直接ブラウザーに送信することも可能です。それには以下の手順に従ってください。

  1. OpenWhisk に戻ります。
  2. 「Develop (開発)」をクリックし、「ShowItems」シーケンスをクリックします。
  3. 「Extend (拡張)」をクリックし、「MY ACTIONS (マイ・アクション)」 > 「inventory_table2html」の順に選択します。
  4. 「+ Add to Sequence (シーケンスに追加)」をクリックします。
  5. 「Save Your Changes (変更内容を保存)」をクリックします。
  6. 左側のサイドバーにある「APIs (API)」をクリックします。
  7. 「Create Managed API + (管理対象 API を作成して追加)」をクリックします。
  8. 以下に記載するパラメーターを使用して API を作成します。
    API name (API 名)Clojure Inventory
    Base path for API (API の基本パス)/clj_inventory
    CORS選択 (CORS を有効にします)
  9. 以下に記載するパラメーターを使用して API 内の処理を作成します。
    Path (パス)/showItems
    Verb (動詞)GET/clj_inventory
    Package containing action (アクションを含めるパッケージ)デフォルト
    Action (アクション)ShowItems
    Response content type (レスポンスのコンテンツ・タイプ)text/html
  10. 「Save & expose (保存して公開)」をクリックします。
  11. 左側のサイドバーにある「API Explorer (API エクスプローラー)」をクリックし、API 内の 1 つのアクション (getShowitems) をクリックして、curl コマンドから URL をコピーします。
  12. コピーした URL をブラウザーに貼り付けて、末尾に
    ?action=getAll
    と入力します。このアクションによって、すべての品目を含むテーブルが返されることを確認します。
  13. action=getAllaction=getAvailable
    で置き換えます。このアクションによって、在庫にある品目だけを含むテーブルが返されることを確認します。

ブラウザーの入力をシーケンスに取り込む

前のセクションでは、URL クエリー・パラメーターを使用して情報を OpenWhisk アクションに送信しました。この方法は action といった 1 つの単純なパラメーターしかないときには有効ですが、大量の情報を送信するとなると、コードが整然としなくなってきます。それよりも、POST を使用して、ユーザーには見えないように情報を送信するほうが賢明です。

そのための方法は 2 つあります。1 つは、ブラウザー内で JavaScript を使用して (できれば Angular などのライブラリーを利用して) JSON を作成し、送信するという方法、もう 1 つは、通常の HTTP フォームを作成して OpenWhisk 内で変換を行うという方法です。OpenWhisk 上で Clojure を使用する方法を学ぶためには、明らかに 2 番目の方法のほうがためになります。

実際にデータを変更するアプリケーションは、2 つのシーケンスで構成されます。最初のシーケンスで、選択可能なすべての品目 (および在庫の数量) を記載するフォームを作成し、2 番目のシーケンスでフォームの結果を処理します。このチュートリアルでは、POS アプリケーションについて説明します。他の 2 つのアプリケーション (再発注と修正) もほぼ同じです。

フォームを作成する

フォームを作成するために、inventory_purchaseForm (ソース・コードを参照) という名前の新しいアクションを作成します。このアクションは inventory_json2HTML アクションとよく似ていますが、テーブルがフォームに埋め込まれるという点、そして購入数を示すための別個の列があるという点で異なります。

このアクションには対処しなければならない 1 つの問題があります。それは、フォームで二重引用符 (") を使用しなければならないことです。あいにく clojure.string/replace の動作の仕組みにより、ストリングに二重引用符が含まれていると、この文字がエスケープされているかどうかにかかわらず、inventory_table2HTML アクションに混乱が生じます。これよりも巧みなソリューションがあるかもしれませんが、時間の関係で、ここでは単純に、二重引用符ではなく 3 つのドル記号 ($$$) を使用します。そして以下のコード内で、inventory_table2HTML からレスポンスを送信する直前にドル記号を二重引用符で置き換えます。

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

次に、called PurchaseForm という名前のシーケンスを作成します。このシーケンスは ShowItems シーケンスと同様ですが、中間アクションは inventory_purchaseForm となっています。作成したシーケンスを API に追加して、このシーケンスを呼び出す API 処理のパスを /purchase、動詞を GET、レスポンスのコンテンツ・タイプを text/html に設定します。

送信されたフォームを処理する

送信される内容そのものと、その内容がどのように利用可能になるのかを調べるための echo アクションを作成しておきました。このアクションは、/purchasePOST 動詞に対する応答として構成されています。このアクションによるレスポンスは以下のとおりです。

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

これで、購入フォームからだけでなく、アクションからもパラメーターを受け取るようになりましたが、(クエリーが組み込まれている URL を変更しなかったため) 3 つの不要な内部パラメーター (__ow_method__ow_headers、および __ow_path) があります。また、行に何も入力されていない場合は、空のストリングを受け取ることになります。

このフォームを、inventory_dbase アクションが理解できる JSON に変換するために、inventory_processPurchase という名前の新しいアクションを作成します。ソース・コードは、このリンク先のページで確認できます。コードの大部分は、これまでに処理してきたコードの内容と同様ですが、以下の点が異なります。

  • データ内の孤立したキーを削除するために filter を使用していますが、条件は少々複雑になっています。or 関数を使用して、5 つの異なる条件を設定しています。
    	realKeys (filter #(not (or (= % "action")
    				     (= % "__ow_method")
                                       (= % "__ow_headers")
                                       (= % "__ow_path")
    				     (= (get usefulParams %) "")
                               )))

    Clojure (およびその他の LISP ベースの言語) に含まれている大半の計算関数は、複数の値を取って処理することができます。その実際の動作を REPL Web サイトで確かめるには、(* 2 3 4 5 6 7) を実行してください。 コマンドの実行結果のスクリーン・キャプチャー
    コマンドの実行結果のスクリーン・キャプチャー

    ご覧のように、すべての数値が乗算されて 5040 という結果が出ています。
  • もう 1 つ異なる点は、reduce を使用してデータ・ハッシュ・テーブルを作成していることです。reduce 関数は、最初のパラメーターとして関数を取り、次に集合 (リスト、ベクトルなど) を取ります。その集合を単一の値に変換するために、パラメーターとして取った関数を最初の 2 つの値に対して実行し、その結果と 3 番目の値に対して同じ関数を実行するといった具体に計算し、最終的に 1 つの値を生成します。数学の用語にすると、f(f(f(v1,v2),v3),v4) のような表現になります。reduce 関数の実際の動作を REPL Web サイトで確かめるには、(reduce #(+ %1 %2) '(1 2 3 4 5 6 7)) を実行してください。 コマンドの実行結果のスクリーン・キャプチャー
    コマンドの実行結果のスクリーン・キャプチャー

    数値がすべて合計されて、結果は 28 になります。

ハッシュ・テーブルを作成するには、assoc 関数を使用します。この関数は最初のパラメーターとしてハッシュ・テーブルを受け入れます。つまり、リストは空のハッシュ・テーブルの状態から始めなければならないということです。それには、list* 関数を使用します。この関数は最後の引数として受け取ったリストの中に、その引数の前にあるすべての引数を挿入します。

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

この実際の動作を REPL Web サイトで確かめるには、(assoc {} :a :b) を実行してください。返されるハッシュ・テーブルには、キーが :a、値が :b の品目が 1 つ含まれています。

今度は (list* 1 2 3 4 5 6 '(7 8)) を実行して、1 ~ 8 の数字からなるリストが結果として返されることを確認してください。

コマンドの実行結果のスクリーン・キャプチャー
コマンドの実行結果のスクリーン・キャプチャー

次は、シーケンスを作成します。以下に、使用するアクションと、それらのアクションを使用する目的を示します。

アクション目的
inventory_processPurchaseデータベース用のデータとアクションを作成します。
inventory_dbaseデータを実際に処理し、品目リストを返します。
inventory_json2html品目リストを HTML テーブルに変換します。
inventory_table2htmlHTML テーブルを完全な Web ページに変換します。

最後に、パスを /purchase、動詞を POST、レスポンスのコンテンツ・タイプを text/html に設定した、このシーケンスを呼び出す API 処理を作成します。これで、結果を処理できるようになります。

他の 2 つのページ (ユーザーが既存のインベントリーを追加するためのページと、データベース内に品目の数量を設定するためのページ) もほぼ同じですが、以下の点が異なります。

  • アクションを開始するために使用するクエリー (action=getAvailable ではなく、action=getAll を使用します)
  • フォームを処理した後のアクションのパラメーター値
  • API のパス

また、再発注を処理する際は、値をストリングに変換する必要があります。そうしなければ、plus 関数が 2 つのストリングを「認識」することになりますが、この関数は JavaScript の plus 関数であることから、2 つのストリングを連結してしまうためです。値をストリングに変換するのに最善の方法は、データベースに対するアクション内で、cljs.reader/parse-int 関数を使用することです。

リポジトリーに登録されたコードを読んで、このコードが自身の作業に関係するかどうかを判断してください。フォームを表示するための新しいシーケンスは必要なく、フォームの表示を処理するシーケンでは、データベースに対する複数の異なるコマンドをサポートするための別のアクションが 1 つ必要になるだけになっています。同じアクションを使用する方法はいくつかありますが、それは不必要にコードを複雑にするだけでしょう。

まとめ

このチュートリアルでは、Clojure で作成する OpenWhisk のアクションを単独のものからシーケンスへと進化させて、実際にブラウザーとやりとりできるようにする方法を説明しました。これでアプリケーションは完成しましたが、現時点ではまるで役に立つものではありません。情報を保管する「データベース」は永続的ではないからです。データベースに対するアクションがしばらく使用されないと、OpenWhisk がそのデータベースによって占有されているメモリーを他の目的に使用したほうが有効であると判断するたびに、データベースは削除されてしまいます。このチュートリアル・シリーズの最終回で、より永続的なストレージ手法を使用する方法を紹介するとともに、その他のこまごまとした詳細について説明します。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Cloud computing
ArticleID=1058933
ArticleTitle=Clojure を使用して OpenWhisk のアクションを作成する, 第 2 回: Clojure OpenWhisk の複数のアクションを結合して有用なシーケンスにする
publish-date=03152018