2012年7月29日日曜日

Webクライアントの実装(3)

はじめに

おはようございます。当ブログにアクセス頂き、ありがとうございます。
鳥のさえずりで、毎日優雅に目覚めている、たなけんです。(私の住んでいるキャンベラは人よりも大型の鳥の方が数が多いようで、叫び声に似た鳴き声で毎日起こされています。)
本エントリでは、Clojurescript(というかClosure Library)でのログ出力の方法および、これまで作成したWebクライアントの、ロジックの分離について記載します。

ログ出力の設定

Closure Libraryではログ出力の為のクラスを提供しており、それをインスタンス化するだけでログを出力することができます。
また、ログの出力先をコンソールにするなど、コンソール環境を整えるクラスのスタティックメソッドを用いて、ログ出力環境を設定することができます。
コードは、本エントリ末尾に掲載します。

ロジックの分離

ロジック分離前のソースコードでは、サーバから渡されたデータをさらにgoogle visualization apiのDataTableクラス用に加工していました。このデータ変換は、クライアント側で行う必要性が薄いため、サーバ側でクライアントが必要するデータの形式に変換するようにしました。
また、clj->js関数については非常に汎用性が高い関数(標準ライブラリに入っていても良さそう)であるため、名前空間my-dictionary-client.utilに移動しました。

ロジック分離後のソースコード(クライアント)

(ns my-dictionary-client.core
(:require
[clojure.browser.event :as event]
[clojure.browser.dom :as dom]
[clojure.browser.repl :as repl]
[goog.net.XhrIo]
[goog.debug.Logger :as logger]
[goog.debug.Console :as console]
[my-dictionary-client.util :as util]))
; set repl
(repl/connect "http://localhost:9000")
; set logger
(def logger (logger/getLogger "my-dictionary-client.core"))
; set console
(console/autoInstall)
; load google visualization api
(.load js/google "visualization" "1.0" (util/clj->js {:packages ["corechart" "table"]}))
; draw the table
(defn- dictionary-callback
[e]
(.draw
(js/google.visualization.Table. (dom/get-element :result))
(js/google.visualization.DataTable. (.getResponseJson e/target))))
; send request to the server and process the response
(defn- dictionary-request
[e]
(goog.net.XhrIo/send
(str "http://localhost:3000/dictionary/" (dom/get-value (dom/get-element :query-word)))
dictionary-callback))
; start listening to the click event in :dictionary-button
(defn- init
[]
(event/listen (dom/get-element :dictionary-button) "click" dictionary-request)
nil)
; init is called when loading the library finished
(.setOnLoadCallback js/google init)
view raw core.cljs hosted with ❤ by GitHub

ログの出力設定後、google visualization apiを読み込み、読み込み後にinit関数を呼び出します。init関数でdictionary-button要素のonClickイベントとdictionary-request関数を結びつけ、ボタン押下時にサーバと通信し、結果を表示します。
ロジックを可能な限りサーバ側へ寄せたことで、クライアント側での処理をライブラリの利用準備とユーザの操作への応答のみに限定することができました。
サーバおよびクライアントの全ソースコードは、githubにて公開していますので、関心のある方は是非ご覧になって下さい。

今回の作業は以上。最後までお読みいただきありがとうございました。
たなけん(作業時間30分)

Clojurescriptでrepl

はじめに

おはようございます。当ブログにアクセス頂き、ありがとうございます。
Clojurescriptの深みに日々はまっている、たなけんです。
本エントリでは、Clojurescriptでのrepl使用方法を記載します。

Clojurescriptのreplの種類

Clojurescriptでは、以下の2種類のreplを利用することができます。

  • Rihno環境
  • Webブラウザ環境

今回は、ブラウザ上でのClojurescriptの挙動を確認するため、後者のWebブラウザ環境でのreplの使用方法について記載します。
Webブラウザ環境のreplでは、cljsbuildプロセスが入出力を処理し、Webブラウザプロセスが入力されたS式を評価します。

cljsbuildによるrepl利用の準備

cljsbuildはコンパイルだけではなく、replの起動、テストの実行といった機能も兼ね備えています。しかし、Webブラウザ環境でreplを起動するには、接続用のhtmlを読み込むClojurescriptおよび接続用のClojurescriptが必要となります。今回はClojurescriptのwikiを例として取り上げますが、実際に開発しているアプリケーションに、数行付け加えるだけでも、Webブラウザ環境のreplを使用することができます。
下記で示される、コードはClojurescriptのrepl利用の手引きからの引用となります。


htmlファイル

<html>
<head>
<meta charset="UTF-8">
<title>Browser-connected REPL</title>
</head>
<body>
<div id="content">
<script type="text/javascript" src="out/goog/base.js"></script>
<script type="text/javascript" src="foo.js"></script>
<script type="text/javascript">
goog.require('foo');
</script>
</div>
</body>
</html>
view raw index.html hosted with ❤ by GitHub

何の変哲も無い、ただ接続用のClojurescriptを読み込むだけのhtmlです。
しかし、1点だけ注意事項があります。それは、必ずボディ部でscript要素を宣言しなくてはいけないということです。
私は当初ヘッド部でJavascriptライブラリの読み込みを宣言しており"Type error: parentElm is null"というエラーに悩まされてしまいました。

接続用のClojurescript

(ns foo
(:require [clojure.browser.repl :as repl]))
(repl/connect "http://localhost:9000/repl")
view raw foo.cljs hosted with ❤ by GitHub

clojure.browser.replを利用し、cljsbuildで起動したポートへ接続します。
clojure.browser.repl/connect関数: 引数のurlへ接続し、replへの入力を受け付ける

起動から接続まで

以下の手順によりreplを起動し、入出力プロセスと評価プロセスを接続します。

  1. コンソールを起動、cljsbuildを利用しているプロジェクトのルートへ移動
  2. コンソールから、lein trampoline cljsbuild repl-listenと入力し、replを起動
  3. Webブラウザを起動し、接続用htmlを読み込む
  4. 同じWebブラウザからurl(デフォルトlocalhost:9000)へアクセスし、cljsbuildのreplと、Webブラウザの接続を確立(接続用のhtmlファイル名がindex.htmlではない場合(例. not-index.html)、urlはhttp://localhost:9000/not-index.htmlのようになる)

replの利用

接続が確立された後は、コンソールから評価したい式を入力することで、ブラウザのdom要素等を操作することができます。
また、load-namespace関数を使うことで、クラスパス内の任意のClojurescriptファイルを読み込むことができます。

今回の作業は以上。最後までお読み頂きありがとうございました。
たなけん(作業時間45分)

2012年7月27日金曜日

Webクライアントの実装(2)

はじめに

おはようございます。当ブログにアクセス頂き、ありがとうございます。
新学期が始まり、そろそろ会計とマネジメントの授業に集中せねばとプレッシャーを感じている、たなけんです。
本エントリでは、Google visualization APIを利用したUIの実装について記載します。

Webクライアントの実装(2)

Closure Libraryでは多彩なUIコンポーネントが用意されています。しかし、Closure LibraryのUIコンポーネントはややオーバースペックであるため、Google visualization APIを利用し、表形式で結果を表示したいと思います。

htmlへの追記

<script type="text/javascript" src="https://www.google.com/jsapi"></script>
view raw dictionary.html hosted with ❤ by GitHub

Google jsapiを利用するため、下記の内容をindex.htmlのヘッダ部に追記しました。

データの変換

(defn clj->js
"Recursively transforms ClojureScript maps into Javascript objects,
other ClojureScript colls into JavaScript arrays, and ClojureScript
keywords into JavaScript strings."
[x]
(cond
(string? x) x
(keyword? x) (name x)
(map? x) (.-strobj (reduce (fn [m [k v]]
(assoc m (clj->js k) (clj->js v))) {} x))
(coll? x) (apply array (map clj->js x))
:else x))
view raw core.cljs hosted with ❤ by GitHub

JSONをClojureオブジェクトへ変換する関数です。
semperosnのgistで公開されていた関数をコピーさせて頂きました。

google visualization API読み込み

(.load js/google "visualization" "1.0" (clj->js {:packages ["corechart" "table"]}))
(defn main []
(.setOnLoadCallback js/google init))
view raw core.cljs hosted with ❤ by GitHub

ライブラリを読み込み、読み込み後にinit関数を実行するよう設定します。

  • google/loadメソッド: ライブラリを読み込む
  • google/setOnLoadCallbackメソッド: ライブラリ読み込み後に実装する関数を指定


表データの作成

(defn- convert-to-table-data
[entry]
{:c [{:v (:pos entry)} {:v (:text entry)}]})
(defn- create-data
[entries]
(clj->js
{:cols
[
{:id "pos" :label "POS" :type "string"}
{:id "text":label "Meaning" :type "string"}]
:rows (vec (for [entry entries] (convert-to-table-data entry)))}))
view raw core.cljs hosted with ❤ by GitHub

サーバから返された値から、google.visualization.Tableクラスのコンストラクタが要求する形式のデータを作成します。

表の描画

(defn- draw-table
[entries]
(.draw
(js/google.visualization.Table. (dom/get-element :result))
(js/google.visualization.DataTable. (create-data entries))))
view raw core.cljs hosted with ❤ by GitHub

div要素resultに表を描画します。
  • js/クラス名 特殊形式: 指定されたクラスをJavaScriptのオブジェクトとして利用する
  • google.visualization.Tableクラス: テーブルを描画するクラス。引数に、描画する位置となる要素を指定する
  • google.visualization.DataTableクラス: データを司るクラス。引数にデータとなるJSONを指定する

今回の作業は以上。最後までお読み頂きありがとうございました。
たなけん(作業時間45分)

2012年7月25日水曜日

クライアント/サーバ間通信でのトラブル

はじめに

おはようございます。当ブログにアクセス頂き、ありがとうございます。
最近になってビリーズブートキャンプに入隊した、たなけんです。
本エントリでは、クライアント/サーバ間通信でのトラブルとその解決方法について記載します。

現象の整理

Webクライアントからサーバにアクセスした際、あるブラウザ(Safari)では期待通りの結果が表示されるが、他のブラウザ(Chrome、Firefox)では期待通りの結果が表示されませんでした。
表示されない原因として、サーバから返されるhttpレスポンスのボディ部に、ブラウザ毎で下記の違いがありました。

  • Safari: httpレスポンスのボディ部にサーバから返されたデータが設定されている
  • Chrome & Firefox: httpレスポンスのボディ部に何も設定されていない


クライアントを疑う

ClojurescriptでのWebクライアントの実装は初めてだったため、もしかしたら何かバグを踏んでいるのではないかと思い、極力シンプルな2つのテストを実施しました。

  • ブラウザから直接アクセス: すべてのブラウザで期待した結果が得られた(リクエストのボディ部に設定したJSON文字列が表示された)
  • jQueryを用いたAjaxクライアントの利用: Clojurescriptと同様、Safariと他のブラウザで異なる結果となった

クライアントコード
jQuery(function($){
$("#btn")
.click(function(){
$.get("http://localhost:3000/dictionary/test",
function(data){
var anobj = data;
$("#result").html(data);
}
);
});
})

これらの結果から、問題はクライアントの実装ではないと判断しました。

サーバ側を変更してみる

Compojureとブラウザ側のXmlHttpRequestとの相性問題などの可能性を考え、2つの異なるhttpサーバを用いたコードを作成し、各Webブラウザより先述したクライアント(jQuery版)からアクセスしたところ、下記の様な結果となりました。

  • Bishop (clojure/ringベースの別のwebサーバ(Web Machineクローンで、Webサービス提供用に機能を特化している)): Clojurescriptと同様、Safariと他のブラウザで異なる結果となった
  • restify (node.js上で動作するWebサーバ用ライブラリ): すべてのブラウザで期待した結果が得られた

Compojure同様clojure/ringベースの別のwebサーバであるBishopを用いても問題は解決せず、node.jsベースのrestifyでは問題なく動作することが分かりました。

トラブルの原因と解決方法

ブラウザからローカルのファイルにアクセスする場合には、ブラウザの起動オプション等を変更する必要があるという様な話は以前より知っていましたが、今回の現象はローカルのWebサーバへのアクセスなので、ブラウザの起動オプションを変更しても問題は解決されませんでした。
通常の開発と今回のとの相違として、httpサーバにアクセスしてindex.htmlを取得し、それをブラウザから操作するのではなく、ローカルで作成したindex.htmlをブラウザから直接開いて操作しているという点に思い至りました。つまり、サーバとしては自分がホストしているドメインとは異なるドメインからアクセスを受けたと認識している訳です。
そこで、外部ドメインからのアクセスを許可するようサーバ側のコードを変更しました。具体的にはhttpヘッダに"Access-Control-Allow-Origin" "*"を追加するよう改修しました。
改修の結果ChromeおよびFirefoxでも期待通りの結果を得ることができました。
今後クライアントサイドMVCフレームワークの流行等で、サーバとクライアントを切り離して開発することも増えるかと思います。その際、同様の問題が発生した場合には、サーバ側でAccess-Control-Allow-Originが適切に設定されているかどうかを確認すると良いという貴重な教訓を得ることができました。


今回の作業は以上。最後までお読み頂きありがとうございました。
たなけん(作業時間120分(トラブルシュート含む))

Webクライアントの実装(1)

はじめに

おはようございます。当ブログにアクセス頂き、ありがとうございます。
昨日美味しい生牡蠣を腹一杯食しご機嫌の、たなけんです。
本エントリでは、Clojurescriptを利用したWebクライアント開発について記載します。

Clojurescriptとは

Clojurescriptとは、ClojureでJavaScriptアプリケーションを開発するためのライブラリ群です。CoffeeScriptやJSX同様、書かれたプログラムをJavaScriptにコンパイルすることによって、アプリケーションをJavaScriptが動作する環境(webブラウザやnode.jsなどのサーバ上)で動作させることができます。

なぜClojurescriptなのか

JavaScriptの言語仕様(変数スコープ、プロトタイプ方式オブジェクト指向など)に馴染めないこと、Clojureスタイルでのプログラミングが私にとって最も効率が良いということからClojurescriptを利用することにしました。
また、ClojurescriptはGoogle Closure Toolsを利用しているため、Closureが提供するクラス方式のオブジェクトシステムや、多彩なUIコンポーネントが利用できる点も魅力でした。

Clojurescriptの導入

ひなたねこさんの記事を参考にlein-cljsbuildを導入しました。lein cljsbuild auto としておけば、core.cljsを更新したタイミングでJavaScriptにコンパイルされます。(他のツールは必要なく、lein-cljsbuildだけで完結している点が素晴らしいです)
また、emacsのClojurescript-modeはELPA経由でインストールしました。
Clojurescript用のreplもemacsから利用可能な様でしたが、今回は出力されたJavaScriptを見てデバッグを行いました。(時間を見つけて、Clojurescriptのreplも試そうと思います)

Webクライアントの実装(1)

プロジェクトファイル

(defproject cljstest "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.4.0"]]
:plugins[[lein-cljsbuild "0.2.4"]]
:cljsbuild {
:builds [{
:source-path "src"
:compiler {
:output-to "main.js"
:pretty-print true}}]})
view raw project.clj hosted with ❤ by GitHub

プラグイン部でlein-cljsbuildの利用を宣言します。また、closure compilerで用いるコンパイルオプションをcljsbuild部で指定します。今回は生成されるJavascriptの可読性を重視したため、最適化はしない方針としました。

index.html

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>cljstest</title>
<script type="text/javascript" src="out/goog/base.js"></script>
<script type="text/javascript" src="main.js"></script>
</head>
<body>
<input id="query-word" value="" type="text" />
<button id="dictionary-button" type="submit">send</button>
<div id="result"></div>
<script>cljstest.core.main();</script>
</body>
</html>
view raw index.html hosted with ❤ by GitHub

ヘッダ部でJavaScriptファイルを指定し、ボディ部の最後でmain関数を呼び出しています。また検索語を入力するフォーム、確定ボタン、結果表示用のdiv要素を宣言しました。

nsマクロ

(ns my-dictionary-client.core
(:require [clojure.browser.event :as event]
[clojure.browser.dom :as dom]
[goog.net.XhrIo])
view raw core.cljs hosted with ❤ by GitHub

通常のClojureとの違いとして、ClojureからJavaのクラスを利用する場合は:requireではなく:importを使用するのですが、ClojurescriptからClosure Libraryのクラスを利用する場合もClojureのライブラリ同様:requireで良いという点があります。
nsマクロでは、以下のライブラリの利用を宣言しました。
  • clojure.browser.event: ブラウザのイベントを操作するClosure Libraryのクラス群の薄いラップしたライブラリ
  • clojure.browser.dom: ブラウザのDOMを操作するClosure Libraryのクラス群の薄いラップしたライブラリ
  • goog.net.XhrIo: Closure Libraryが提供するJacascriptのhttpXmlRequestオブジェクトを操作するクラス


main関数

(defn main
[]
(event/listen (dom/get-element :dictionary-button) "click" dictionary-request))
view raw core.cljs hosted with ❤ by GitHub

確定ボタンのクリックイベントをトリガーにdictionary-request関数を呼びます。

  • clojure.browser.event/listen関数: 引数に指定された要素、イベントによって第3引数で指定された関数が呼び出されるよう設定
  • clojure.browser.dom/get-element関数: 引数に指定されたシンボルのidに合致する要素を取得

生成されたJavaScript
cljstest.core.main = function main() {
return clojure.browser.event.listen.call(null, clojure.browser.dom.get_element.call(null, "\ufdd0'dictionary-button"), "click", cljstest.core.dictionary_request)
};
view raw main.js hosted with ❤ by GitHub


dictionary-request関数

(defn- dictionary-request
[e]
(goog.net.XhrIo/send
(str "http://localhost:3000/dictionary/" (dom/get-value (dom/get-element :query-word)))
dictionary-callback))
view raw core.cljs hosted with ❤ by GitHub

httpのGETメソッドによるリクエストを送信し、結果を引数にコールバック関数を呼び出します。
  • goog.net.XhrIo/sendメソッド: 引数のurlにhttpのGETメソッドによるリクエストを送信
  • clojure.browser.dom/get-value関数: 引数に指定された要素の値(今回の場合、フォームquery-wordに入力された文字列)を取得
生成されたJavaScript
cljstest.core.dictionary_request = function dictionary_request(e) {
return goog.net.XhrIo.send([cljs.core.str("http://localhost:3000/dictionary/"), cljs.core.str(clojure.browser.dom.get_value.call(null, clojure.browser.dom.get_element.call(null, "\ufdd0'query-word")))].join(""), cljstest.core.dictionary_callback)
};
view raw main.js hosted with ❤ by GitHub


dictionary-callback関数

(defn- dictionary-callback
[e]
(let [entries (.-entries (.getResponseJson e/target))]
(dom/append (dom/get-element :result) (create-list entries))))
view raw core.cljs hosted with ❤ by GitHub

httpレスポンスからJSONオブジェクトを取得し、create-list関数を用いて結果を変換し、変換された結果をdiv要素の子要素として追加します。
  • .-プロパティ名 特殊形式: JavaScriptオブジェクトのプロパティにアクセス(今回の場合entriesプロパティ)
  • goog.net.XhrIo/getResponseJsonメソッド: 取得されたレスポンス内のデータをJSONオブジェクトとして取得
  • clojure.browser.dom/append関数: 第1引数に指定された要素に、第2引数に指定された要素を子要素として追加
生成されたJavaScript
cljstest.core.dictionary_callback = function dictionary_callback(e) {
var entries__212463 = e.target.getResponseJson().entries;
return clojure.browser.dom.append.call(null, clojure.browser.dom.get_element.call(null, "\ufdd0'result"), cljstest.core.create_list.call(null, entries__212463))
};
view raw main.js hosted with ❤ by GitHub

create-list関数

(defn- create-list
[entries]
(dom/html->dom
(str "<ul>"
(apply str (for [entry entries] (str "<li>" (.-text entry) "</li>")))
"</ul>")))
view raw core.cljs hosted with ❤ by GitHub

JSONオブジェクトを引数に取り、textプロパティから値を取得し、<ul><li>textプロパティの値</li> ... </ul>形式のdom要素を作成します。

  • clojure.browser.dom/html->dom関数: html形式の文字列からDOMツリーを生成


生成されたJavaScript
cljstest.core.create_list = function create_list(entries) {
return clojure.browser.dom.html__GT_dom.call(null, [cljs.core.str("<ul>"), cljs.core.str(cljs.core.apply.call(null, cljs.core.str, function() {
var iter__2482__auto____212461 = function iter__212455(s__212456) {
return new cljs.core.LazySeq(null, false, function() {
var s__212456__212459 = s__212456;
while(true) {
if(cljs.core.seq.call(null, s__212456__212459)) {
var entry__212460 = cljs.core.first.call(null, s__212456__212459);
return cljs.core.cons.call(null, [cljs.core.str("<li>"), cljs.core.str(entry__212460.text), cljs.core.str("</li>")].join(""), iter__212455.call(null, cljs.core.rest.call(null, s__212456__212459)))
}else {
return null
}
break
}
}, null)
};
return iter__2482__auto____212461.call(null, entries)
}())), cljs.core.str("</ul>")].join(""))
};
view raw core.cljs hosted with ❤ by GitHub

意外な落とし穴

実装を終えて早速クライアントからサーバへの疎通を確認しようと、ブラウザからアクセスしたところ、Safariでは問題なくレスポンスが処理されるが、Firefoxではレスポンスのボディ部が0バイト(サーバ側で設定したはずの値が、ボディ部に含まれていない)様な挙動を見せ、想定した通りの処理をすることができませんでした。
ブラウザ毎に異なる挙動はClosureライブラリで吸収されているはず、と思いながらも、ここから問題解決まで思ったよりも時間が掛かってしまいました。。。(次回に続く)


今回の作業は以上。最後までお読み頂きありがとうございました。
たなけん (作業時間90分)

2012年7月23日月曜日

第2イテレーションの計画

はじめに

おはようございます。当ブログにアクセス頂き、ありがとうございます。
『今やる、すぐやる、出来るまでやる』がモットーの、たなけんです。
本エントリでは、第2イテレーションの計画として、仕様および実装方針を記載します。


第2イテレーション(プレゼンテーション層/Webクライアント)の仕様

第2イテレーションで実装する機能の仕様は以下の通り。
  • 定義を取得したい単語をhttpリクエストを送信する
  • 外部APIからの抽出結果をhttpレスポンスとして受信する
  • 受信したデータを整形して

実装方針

  • クライアントはWebブラウザとし、Ajax通信によりサーバへ問い合わせる
  • クライアントプログラムの実装には、Clojurescript / Google Closure Libraryを用いる
  • CSSの作成にSCSS(Sass)を用いる

テスト方針

  • JsUnitを用いて単体テストを実施する
  • PhantomJSを用いて単体テストを自動化する
  • Seleniumにより結合テストを実施する


今回の作業は以上。最後までお読み頂きありがとうございました。
たなけん (作業時間10分)

2012年7月15日日曜日

第1イテレーションの振り返り

はじめに

おはようございます。当ブログにアクセス頂き、ありがとうございます。、たなけんです。本エントリでは、第1イテレーションを振り返ります。


第1イテレーションの振り返り

普段はツール開発にClojureを利用していましたが。今回のアプリケーション開発では、堅牢なアプリケーション開発のために以下の点を意識しました。

  • ライブラリの利用
  • ツールの利用
  • 実装


ライブラリの利用

多数のライブラリを利用しましたが、その中でも下記のライブラリについて利用した感想を簡単に紹介したいと思います。

  • clojure.tools.logging: ログ出力。副作用のある処理なのでdoと合わせて使うと便利
  • clj-xpath: xpath問い合わせ。完結かつ柔軟にxmlノードを抽出できて便利
  • clojure.test: テストコード定義。Leiningenからもテストが実行可能で便利


ツールの利用

実際にツールを利用した感想を以下に記します。

  • IDE: emacs。起動、終了が高速かつ、replを利用しながらテスト可能でありEclipceよりも使い勝手が良い印象
  • バージョン管理: git。特別な利用をしていないためSubversionなどと同様な印象
  • 継続的インテグレーション: Jenkins。まだ運用方法が未確立



実装

実装時に様々なClojureの機能や関数を利用しましたが、その中でも便利だったものを以下に紹介します。


  • 例外処理: gen-classを使用。Exceptionを継承することでアプリケーション例外を定義
  • 関数の公開範囲: 他の名前空間から呼ばれない関数はdefn-で定義
  • reduce関数: ネストされたハッシュから値を抽出する際に利用




今回の作業は以上。最後までお読み頂きありがとうございました。
たなけん (作業時間30分)

アプリケーションのテスト(3)

はじめに

おはようございます。当ブログにアクセス頂き、ありがとうございます。夜6時以降は炭水化物を摂取しない、たなけんです。本エントリでは、アプリケーションのテスト(3)について記載します。

アプリケーションのテスト(3)

関数の機能を変更したため、テストコードも変更しました。
テスト事項は以下となります。
  • interface-for-clientのテスト
  • call-apiのテスト
  • 抽出関数のテスト
また、以下のテストデータを追加しました。
  • test/data/test-dictionary.xml
  • test/data/test-thesaurus.xml
  • test/data/test-slang.xml
  • test/data/test-etymology.xml
  • test/data/test-example.xml
  • test/data/test-questionanswer.xml
  • test/data/test-synonym.xml
  • test/data/test-random-dictionary.xml
  • test/data/test-random-thesaurus.xml
  • test/data/test-spelling.xml
  • test/data/test-wotd.xml

interface-for-clientのテスト

(deftest test-interfece-for-client-dictionary
(is (:status (interface-for-client {:request-method :get :uri "/dictionary/define/test"}))
200))
view raw core_test.clj hosted with ❤ by GitHub

httpのGETリクエストからcall-apiを呼び出し、正常に処理されることを確認します。

call-apiのテスト

(deftest test-call-api
(is (call-api root-url (list [:vid (:vid test-properties)] ["q" "test"] ["type" "define"] ["site" "dictionary"]) extract-dictionary)
(json/generate-string test-dictionary-result)))
view raw core_test.clj hosted with ❤ by GitHub

url、パラメータ及び抽出関数を引数に、call-apiが期待する動作をするかどうかを確認します。

抽出関数のテスト

(deftest test-extract-dictionary
(is (extract-dictionary test-dictionary-xml)
test-dictionary-result))
(deftest test-extract-example
(is (extract-example test-example-xml)
test-example-result))
(deftest test-extract-spelling
(is (extract-spelling test-spelling-xml)
test-spelling-result))
(def test-dictionary-result {:word "test", :entries '({:pos "noun", :text "the means used to determine the quality, content, etc., of something"} {:pos "noun", :text "examination to evaluate a student or class"} {:pos "verb (used with object)", :text "to subject to a test"})})
(def test-example-result {:word "test", :entries '("noun : to put to the test. <br>verb (used without object) : People test better in a relaxed environment. <ex>,</ex>to test for diabetes. <br>.")})
(def test-spelling-result {:word "teest", :entries '("testy" "tees" "weest" "deist" "doest" "toast" "reset" "retest" "tersest" "truest" "teased" "teat" "tee's" "teed" "tests" "Tevet" "detest" "tenet" "tester" "tweet" "teats" "Tet" "taste" "tasty" "EST" "beset" "desert" "est" "tamest" "DST" "teds" "Tess" "Tues" "teas" "text" "ties" "toes" "attest" "Ted's")})
view raw core_test.clj hosted with ❤ by GitHub

与えられたxmlから、期待される値が抽出されることを確認します。
また期待される値を定数として定義します。

テストデータの登録

(def test-dictionary-xml (slurp "test/data/test-dictionary.xml"))
(def test-thesaurus-xml (slurp "test/data/test-thesaurus.xml"))
(def test-slang-xml (slurp "test/data/test-slang.xml"))
(def test-etymology-xml (slurp "test/data/test-etymology.xml"))
(def test-example-xml (slurp "test/data/test-example.xml"))
(def test-questionanswer-xml (slurp "test/data/test-questionanswer.xml"))
(def test-synonym-xml (slurp "test/data/test-synonym.xml"))
(def test-random-dictionary-xml (slurp "test/data/test-random-dictionary.xml"))
(def test-random-thesaurus-xml (slurp "test/data/test-random-thesaurus.xml"))
(def test-spelling-xml (slurp "test/data/test-spelling.xml"))
(def test-thesaurus-xml (slurp "test/data/test-wotd.xml"))
view raw core_test.clj hosted with ❤ by GitHub

Dictionary.comの各APIにアクセスした際に取得されるxmlを定義します。

今回の作業は以上。最後までお読み頂きありがとうございました。
たなけん (作業時間30分)

2012年7月14日土曜日

クライアント向けインターフェースの実装(2)

はじめに

おはようございます。当ブログにアクセス頂き、ありがとうございます。好きな言語は?と聞かれたら、Clojureと即答する、たなけんです。本エントリでは、プレゼンテーション層とアプリケーション層とをつなぐ、クライアント向けインターフェースの実装について記載します。

クライアント向けインターフェースの実装(2)

前回リストアップした以下の項目に従い、各関数を修正します。
  • interface-for-clientへのリクエストの処理を追加
  • 関数を引数に取り、抽出結果のJSON文字列を生成する関数の追加
  • 利用APIに対応した抽出関数の追加

interface-for-clientへのリクエストの処理

(def root-url "http://api-pub.dictionary.com/v001")
(compojure-core/defroutes interface-for-client
;
(compojure-core/GET "/dictionary/:word" [word]
(call-api root-url
(list
[:vid (:vid properties)] ["q" word] ["type" "define"] ["site" "dictionary"])
extract-dictionary))
;
(compojure-core/GET "/example/:word" [word]
(call-api root-url
(list
[:vid (:vid properties)] ["q" word] ["type" "example"])
extract-example))
;
(compojure-core/context "/random" []
("/dictionary" []
(call-api root-url
(list
[:vid (:vid properties)] ["type" "random"] ["site" "dictionary"])
extract-random))
("/thesaurus" []
(call-api root-url
(list
[:vid (:vid properties)] ["type" "random"] ["site" "thesaurus"])
extract-random)))
;
(compojure-core/GET "/spelling/:word" [word]
(call-api root-url
(list
[:vid (:vid properties)] ["q" word] ["type" "spelling"])
extract-spelling))
;
(compojure-route/not-found "Page not found"))
view raw core.clj hosted with ❤ by GitHub

urlリソースと関数を関連付けます。例えば、http://localhost/dictionary/testへアクセスすると、引数のwordに"test"が拘束された状態で、call-apiが呼び出されます。

  • compojure.core/defroutesマクロ: urlリソースを定義
  • compojure.core/GETマクロ: http GETメソッドによりアクセスされる節を構成
  • call-api関数: url、パラメータリストおよび抽出関数を引数に取り、JSON文字列を生成


抽出結果のJSON文字列を生成する関数

(defn call-api
[url prms extract-function]
(-> (build-url-with-prms url prms)
(get-body ,)
(extract-function ,)
(json/generate-string ,)))
(defn build-url-with-prms
"to build url string with parameters"
[url prms]
(str url (string/replace-first (apply str (for [[k v] prms] (str "&" (name k) "=" v))) "&" "?")))
(defn get-body
"to send request and get body element of the response"
[url]
(:body (try
(http-client/get url)
(catch java.io.IOException ioe (handle-exception ioe)))))
view raw core.clj hosted with ❤ by GitHub

抽出関数を引数に取り、Dictionary.comから取得したxmlから必要な情報を抽出した結果のJSON文字列を生成します。

  • ->マクロ: 左の関数を評価した結果を右の関数の引数とし、順次関数を実行
  • build-url-with-prms関数: urlパラメータのリストからurl文字列を生成
  • get-body関数: 引数のurlへhttpリクエストを送信し、そのhttpレスポンスのbody要素を取得
  • extract-function(引数): 関数呼び出し時に指定される抽出関数
  • clj-json.core/generate-string関数: clojureオブジェクトをJSON文字列に変換

抽出関数

(defn safety-xpath-call
"to call xpath safely"
[xpath xml]
(try
(xpath/$x xpath xml)
(catch org.xml.sax.SAXException saxe (handle-exception saxe))))
(defn get-result
"to extract text from a xml-string"
[xpath xml-string key-list]
(for [line (safety-xpath-call xpath xml-string)]
(reduce get line key-list)))
(defn- get-query-word
"to extract query word from a xml-string"
[xml-string]
(first (get-result "(/*)[1]" xml-string '(:attrs :query))))
(defn extract-dictionary
"to extract word and entries from a xml-string of thesaurus"
[xml-string]
{:word (get-query-word xml-string)
:entries
(flatten
(for [pos (get-result "//partofspeech" xml-string '(:attrs :pos))]
(for [line (get-result (str "//partofspeech[@pos=\"" pos "\"]/defset/def") xml-string '(:text))] {:pos pos :text line})))})
(defn extract-example
"to extract word and entries from a xml-string of example"
[xml-string]
{:word (get-query-word xml-string)
:entries (get-result "//example" xml-string '(:text))})
(defn extract-random
"to extract word from a xml-string of random"
[xml-string site]
{:word (first (get-result (str "//" site "/random_entry") xml-string '(:text)))})
(defn extract-spelling
"to extract word and entries from a xml-string of spelling"
[xml-string]
{:word (get-query-word xml-string)
:entries (get-result "//suggestion" xml-string '(:text))})
view raw core.clj hosted with ❤ by GitHub

xpathによりxmlからノードを取得し、さらにそこから指定した要素を抽出します。

  • safety-xpath-call関数: xpathによりxmlからノードを取得する。指定したxpathが取得出来なかった場合、handle-exception関数を呼び出す
  • get-result関数: safety-xpath-call関数により取得したxmlノード(ネストした状態のハッシュ
  • )から、順にキーを指定して、求める値を取得する
  • get-query-word関数: xml内に含まれる、問い合わせ単語を取得する。具体的にはxpathが(/*)[1]のノードから、キー:attrs :queryの値を取得する。
  • extract-dictionary関数: 英英辞書APIから取得したxmlから、問い合わせ単語と定義を抽出する。
  • extract-example関数: 例文辞書APIから取得したxmlから、問い合わせ単語と例文を抽出する。
  • extract-random関数: ランダムAPIから取得したxmlから、単語を抽出する。
  • extract-spelling関数: 綴り時APIから取得したxmlから、問い合わせ単語と綴り字の候補を抽出する。





今回の作業は以上。最後までお読み頂きありがとうございました。
たなけん (作業時間120分)






2012年7月11日水曜日

クライアント向けインターフェースの実装(1)

はじめに

おはようございます。当ブログにアクセス頂き、ありがとうございます。
好きな料理は?と聞かれたら、カレーと即答する、たなけんです。
本エントリでは、プレゼンテーション層とアプリケーション層とをつなぐ、クライアント向けインターフェースの実装について記載します。


クライアント向けインターフェースの実装(1)

プロジェクトファイル

(defproject my-dictionary "0.1.0-SNAPSHOT"
:description "A tiny web application for requesting Dictionary.com web api"
:url "http://"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [
[org.clojure/clojure "1.3.0"]
[org.clojure/tools.logging "0.2.3"]
[log4j/log4j "1.2.16"]
[log4j/apache-log4j-extras "1.1"]
[clj-http "0.4.3"]
[clj-json "0.5.0"]
[clj-xpath "1.3.0"]
[compojure "1.1.0"]
[ring/ring-jetty-adapter "1.1.1"]]
:ring {:handler my-dictionary.core/interface-for-client})
view raw project.clj hosted with ❤ by GitHub

Compojure ver 1.1.0の利用を追記しました
またleiningenからringアプリケーションを起動出来るよう:ringオプションを追記しました。

nsマクロ

(ns my-dictionary.core
(:require
:reload-all
[clojure.tools.logging :as logging]
[clj-http.client :as http-client]
[clj-xpath.core :as xpath]
[clj-json.core :as json]
[compojure.core :as compojure-core]
[compojure.route :as compojure-route])
(:import
(java.io.IOException)
(org.xml.sax.SAXException)
(myDictionary.java.AppException)))
view raw core.clj hosted with ❤ by GitHub

compojure.coreとcompojure.routeの利用を追記しました。

仮実装

(compojure-core/defroutes interface-for-client
(compojure-core/GET "/dictionary/define/:word" [word] (from-build-url-to-generate-json word))
(compojure-route/not-found "Page not found"))
view raw core.clj hosted with ❤ by GitHub

httpクライアントからアクセスを可能にするためurlと関数を結びつけました。

テストコード

(deftest test-interfece-for-client
(is (interface-for-client {:request-method :get :uri "/dictionary/define/test"})
{:status 200, :headers {"Content-Type" "text/html; charset=utf-8"}, :body "{\"word\":\"test\",\"entries\":[{\"pos\":\"noun\",\"mean\":\"the means used to determine the quality, content, etc., of something\"},{\"pos\":\"noun\",\"mean\":\"examination to evaluate a student or class\"},{\"pos\":\"verb (used with object)\",\"mean\":\"to subject to a test\"}]}"}))
(deftest test-interface-for-client-404
(is (interface-for-client {:request-method :get :uri ""})
{:status 404, :headers {"Content-Type" "text/html; charset=utf-8"}, :body "Page not found"}))
view raw core_test.clj hosted with ❤ by GitHub

正常系、以上系の処理の2パターンを追加しました。

対応するAPIを増やす(準備)

これまでDictionary.comのdefine/dictionaryのみを対象に、url構築からjson-文字列抽出を実装しましたが、Dictionary.comではdefinition/dictionary以外にも様々なAPIを提供されています。
そこで、折角ですのでdefine/dictionary以外のAPIにも対応できるよう、これまで実装した関数に変更を加えようと思います。(修正に伴い、関数ごとの機能の切り分けなど、プログラムの見通しも良くしたいと思います。)
表1: Dictionary.comのAPI一覧

サービス名typesite

Dictionary (英英辞書)definedictionary(任意)

Thesaurus (分類語彙辞書)definethesaurus(任意)

Slang (俗語辞書)defineslang(任意)

Etymology (語源)無しetymology

Example (例文)example無し

Questions and Answers (Q&A)無しquestionanswer

Synonyms (同義語)synonyms無し

Random (ランダム)randomdictionary(任意)

randomthesaurus(任意)

Spelling (綴り字)spelling無し

まずはhttpリクエストのパラメータについて整理します。
英英辞典、分類語彙辞書、俗語辞書、ランダムについては、同じ引数でurlを組み立てることができます。(ただしsiteは省略可能、省略時にはdictionaryが指定されます)
しかし例文、同義語、綴り時については上記とは異なり引数にsiteを取りません。また、語源とQ&Aについては、引数にsiteを取らない代わりにtypeを取ります。
つまり、urlのパターンは以下のようになります。
  • typeにdefineが指定される
  • typeにdefineが指定され、siteにdictionaryが指定される
  • typeにdefineが指定され、siteにthesaurusが指定される
  • typeにdefineが指定され、siteにslangが指定される
  • siteにetymologyが指定る
  • typeにsynonymsが指定される
  • typeにrandomが指定される
  • typeにrandomが指定され、siteにdictionaryが指定される
  • typeにrandomが指定され、siteにthesaurusが指定される
  • typeにspellingが指定される
現時点では、引数が固定のため、語源、例文、Q&A、同義語、綴り時のサービスに対応していません。また、レスポンスとして返されるxmlのフォーマットも異なるため、各サービスごとにxpathを変更する必要があります。
つまり、対応APIを増やす為に必要な修正点は以下となります。
  • interface-for-clientで呼び出しサービスを識別
  • 利用するAPIに対応したurlを構築
  • 利用するAPIに対応したxmlからのJSON文字列の生成


今回の作業は以上。最後までお読み頂きありがとうございました。
たなけん (作業時間30分)

2012年7月8日日曜日

アプリケーションのテスト(2)とCIツールの導入

はじめに

おはようございます。当ブログにアクセス頂き、ありがとうございます。
『名は体を表わす』の通り、健康でふくよかボディのたなけんです。
本エントリでは、アプリケーションのテストおよびCIツールの導入について記載いたします。


アプリケーションのテスト(2)

本日は以下のテストコードを紹介します。
  • xml抽出関数のテスト
  • JSON生成関数のテスト
  • url構築からJSON文字列生成までを結合した関数のテスト

xml抽出関数のテスト

(deftest test-extract-xml
(is (core/extract-xml test-url)
test-xml))
(deftest test-extract-xml-ioe
(is (thrown? myDictionary.java.AppException
(core/extract-xml ""))))
view raw core_test.clj hosted with ❤ by GitHub

  • 正常系: 正常urlを引数に、想定されるxmlが抽出されるかをテスト
  • 異常系: 異常urlを引数に、想定される例外が発生するかをテスト

JSON生成関数のテスト

(deftest test-generate-json
(is (core/generate-json test-xml)
result-string))
(deftest test-generate-json-saxe
(is (thrown? myDictionary.java.AppException
(core/generate-json ""))))
view raw core_test.clj hosted with ❤ by GitHub

  • 正常系: 正常フォーマットのxml文字列を引数に、想定されるJSON文字列が生成されるかをテスト
  • 異常系: 異常フォーマットのxml文字列を引数に、想定される例外が発生するかをテスト

url構築からJSON文字列生成までを結合した関数のテスト

正常系: 単語を引数に、想定されるJSON文字列が生成されるかをテスト

CIツールの導入

参考にさせて頂いたサイトのリンクをご紹介いたします。

  • Jenkinsのインストール Link
  • JenkinsとLeiningenの連携 Link


今回の作業は以上。最後までお読み頂きありがとうございました。
たなけん (作業時間30分(各種インストールは除く))

アプリケーションのテスト(1)


はじめに

おはようございます。当ブログにアクセス頂き、ありがとうございます。
『やってみせ、言って聞かせて、させてみて、褒めてやらねば人は動かじ』は、少し過保護なのではないかとふと思う、たなけんです。
本エントリでは、アプリケーションのテストを記載いたします。


アプリケーションのテスト(1)

本日は以下のテストコードを紹介します。
  • nsマクロ及び定数
  • 設定ファイル読み込み関数のテスト
  • url構築関数のテスト

nsマクロ及び定数

(ns my-dictionary.core-test
(:use
[clojure.test :only (deftest is)])
(:require
:reload-all
[clojure.test :as test]
[my-dictionary.core :as core])
(:import
(myDictionary.java.AppException)
(java.lang.RuntimeException)))
(def test-url (str
"http://api-pub.dictionary.com/v001?vid="
(:vid core/properties)
"&q=test&type=define&site=dictionary"))
(def test-xml (slurp "test/data/test.xml"))
(def result-string "{\"word\":\"test\",\"entries\":[{\"pos\":\"noun\",\"mean\":\"the means used to determine the quality, content, etc., of something\"},{\"pos\":\"noun\",\"mean\":\"examination to evaluate a student or class\"},{\"pos\":\"verb (used with object)\",\"mean\":\"to subject to a test\"}]}")
view raw core_test.clj hosted with ❤ by GitHub

名前空間および依存ライブラリの宣言をします。
また、テストの検証に用いる定数も宣言します。

設定ファイル読み込み関数のテスト

(deftest test-load-properties
(is (core/load-properties "test/data/test.s")
{:test "test"}))
(deftest test-load-properties-ioe
(is (thrown? myDictionary.java.AppException
(core/load-properties "wrong-file-path"))))
(deftest test-load-properties-re
(is (thrown? java.lang.RuntimeException
(core/load-properties "test/data/test-wrong.s"))))
view raw core_test.clj hosted with ❤ by GitHub

設定ファイル読み込み関数をテストします。エラーの場合は想定されるExceptionかどうか判定します。

  • 正常系: 正しいフォーマットのファイル読み込み (記載内容 {:test "test"})
  • 異常系 1: 設定ファイルが存在しない場合のエラーハンドリング
  • 異常系 2: 設定ファイルの記載内容が異常の場合のエラーハンドリング (記載内容 {:test "test")

url構築関数のテスト

(deftest test-build-url
(is (core/build-url
"api-pub.dictionary.com"
(:vid core/properties)
"test"
"define"
"dictionary")
test-url))
view raw core_test.clj hosted with ❤ by GitHub

引数をつなげた文字列が生成されるかを確認します。

今回の作業は以上。最後までお読み頂きありがとうございました。
たなけん (作業時間30分)


アプリケーションの実装(3)とバージョン管理ツールの導入


はじめに

おはようございます。当ブログにアクセス頂き、ありがとうございます。
お腹が空くと『欲しがりません勝つまでは』と言っている、たなけんです。
本エントリでは、アプリケーションの実装とバージョン管理ツールの導入について記載いたします。


アプリケーションの実装(3)

本日は、url構築からJSON文字列生成までを結合した関数のコードを紹介します。

url構築からJSON文字列生成までを結合した関数

(defn from-build-url-to-generate-json
"to combine three functions"
[word]
(-> (build-url "api-pub.dictionary.com" (:vid properties) word "define" "dictionary")
(extract-xml ,)
(generate-json ,)))
view raw core.clj hosted with ❤ by GitHub

引数に単語を取り、戻り値に意味を抽出したJSON文字列を返します。
  • ->マクロ: 左からS式を評価し、その返り値を次のS式の引数として、順次S式を評価する

    バージョン管理ツールの導入

    参考にさせて頂いたサイトのリンクをご紹介いたします。
    • gitのインストール Link
    • GitHubアカウント作成 Link
    • リポジトリへの追加 Link
    • 変更のコミット Link
    上記手順でGitHubにコードを登録&公開しております。
    ご関心のある方は、是非ご一読下さい。 Link


    今回の作業は以上。最後までお読み頂きありがとうございました。
    たなけん (作業時間30分(各種インストールは除く))

    アプリケーションの実装(2)

    はじめに

    おはようございます。当ブログにアクセス頂き、ありがとうございます。
    『臥薪嘗胆』が高校時代の座右の銘だった、たなけんです。
    本エントリでは、アプリケーションの実装について記載いたします。

    アプリケーションの実装(2)

    本日は以下のコードを紹介します。
    • url構築関数
    • xml取得関数
    • JSON文字列出力関数
    • 意味検索メイン関数

    url構築関数

    (defn build-url
    "to build url string with parameters"
    [host vid q type site]
    (apply str "http://" host "/v001?vid=" vid "&q=" q "&type=" type "&site=" site))
    view raw core.clj hosted with ❤ by GitHub

    引数に与えられた文字列をurlテンプレートに埋め込みます。
    Dictionary.comは意味取得以外にもAPIを提供しているため、
    柔軟にurlを構築出来るよう引数を設定しています。 
    • str関数: 引数を文字列として結合する

    xml取得関数

    (defn extract-xml
    "to send request and extract xml part from reply"
    [url]
    (:body (try
    (http-client/get url)
    (catch java.io.IOException ioe (handle-exception ioe)))))
    view raw core.clj hosted with ❤ by GitHub

    引数のurlへhttpリクエストを送信し、レスポンスを受信します。
    レスポンスのbody要素をxml文字列として取得します。

    • clj-http.client/get関数: 引数のurlにGETメソッドのhttpリクエストを送信

    JSON文字列出力関数

    (defn generate-json
    "to return a hashmap including word and entries"
    [xml-string]
    ;; local function to call xpath/$x safely
    (letfn [(safety-xpath-call
    [xpath xml]
    (try
    (xpath/$x xpath xml)
    (catch org.xml.sax.SAXException saxe (handle-exception saxe))))] ; end of letfn safety-xpath-call
    (let [word (:query (:attrs (first (safety-xpath-call "//dictionary" xml-string))))
    entries (let [pos (for [line (safety-xpath-call "//partofspeech" xml-string)] (:pos (:attrs line)))] ; end of let pos
    ; local function to extract mean from //partofspeach/defset/def
    (letfn [(get-mean
    [each-pos xml-string]
    (for [line (safety-xpath-call (str "//partofspeech[@pos=\"" each-pos "\"]/defset/def") xml-string)] (:text line)))] ; end of letfn get-mean
    ; generating entries
    (for [i pos]
    (for [j (get-mean i xml-string)] {:pos i :mean j}))))] ; end of let word, entries
    ; get-dictionary-define-json (cont.)
    (json/generate-string {:word word :entries (flatten entries)}))))
    view raw core.clj hosted with ❤ by GitHub

    xml文字列から、単語、品詞、意味を含むJSON文字列を生成します。

    • letfn特殊形式: スコープ内でのみ利用する関数を束縛
    • clj-xpath.core/$x: 第1引数のxpath問い合わせを、第2引数のxmlに対して実行する
    • let特殊形式: スコープ内でのみ利用する変数を束縛
    • for特殊形式: リスト内包表記
    • clj-json.core/generate-string: ClojureオブジェクトからJSON文字列を生成する

    例えば下記のようなxmlから
    <?xml version="1.0" encoding="UTF-8" ?>
    <dictionary query="test" totalresults="1">
    <entry source="pdict" id="4162883">
    <display_form ><![CDATA[test]]></display_form>
    <pron><![CDATA[[test]]]></pron>
    <partofspeech pos="noun">
    <defset>
    <def charcnt="68" defno="1"><![CDATA[the means used to determine the quality, content, etc., of something]]></def>
    <def charcnt="42" defno="2"><![CDATA[examination to evaluate a student or class]]></def>
    </defset>
    </partofspeech>
    <partofspeech pos="verb (used with object)">
    <defset>
    <def charcnt="20" defno="3"><![CDATA[to subject to a test]]></def>
    </defset>
    </partofspeech>
    </entry>
    </dictionary>
    view raw test.xml hosted with ❤ by GitHub

    下記のようなJSONを作成します。(改行、エスケープシーケンスは若干異なります)
    {
    "word":"test",
    "entries":[
    {"pos":"noun",
    "mean":"the means used to determine the quality, content, etc., of something"},
    {"pos":"noun",
    "mean":"examination to evaluate a student or class"},
    {"pos":"verb (used with object)",
    "mean":"to subject to a test"}
    ]
    }
    view raw result.json hosted with ❤ by GitHub


    追記: リスト内包表記について

    例えば、(for [i '(1 2 3)] (+ 10 i))を評価すると、(11 12 13)が結果として返されます。
    動作のイメージとしては、forの後の角括弧内のiにリストの各要素が束縛され、角括弧の後のS式を評価した結果のリストを返すといったものです。
    1変数の場合は、map関数を用いても同様の結果を得ることが出来ます。
    (map #(+ 10 %) '(1 2 3))

    forの場合は複数の変数や、階層を持たせる(入れ子のfor文のイメージ)ことができます。
    複数の変数を利用すると、Pythonのタプルのような、2要素のhashmapを簡単に作ることができます。

    複数変数の例 (for [i '(:a :b :c) j '(1 2 3)] {i j})
    結果 ({:a 1} {:a 2} {:a 3} {:b 1} {:b 2} {:b 3} {:c 1} {:c 2} {:c 3}) 

    入れ子の例
    (for [i '(:a :b :c)]
      (for [j '(1 2 3)] {i j})
    )
    結果

    (
      ({:a 1} {:a 2} {:a 3})
      ({:b 1} {:b 2} {:b 3})
      ({:c 1} {:c 2} {:c 3})
    )


    今回の作業は以上。最後までお読み頂きありがとうございました。
    たなけん (作業時間45分)

    開発環境整備とアプリケーションの実装(1)

    はじめに

    おはようございます。当ブログにアクセス頂き、ありがとうございます。
    『千里の道も一歩から』とつぶやきながら、人生を迷走しているたなけんです。
    本エントリではClojureを用いたWebアプリケーション開発の第一歩として、仕様策定(全体, 第1イテレーション)と開発環境の整備、そして実装について記載いたします。

    アプリケーションの全体像

    クライアントからhttp GETメソッドによって、サーバに問い合わせ、
    外部APIから取得した情報をhttpレスポンスとして返す、
    非常にシンプルなWebアプリケーションとなります。
    • 入力: クライアントにより入力された単語を受信する
    • 処理: 外部APIを利用し、英単語の意味を取得する
    • 出力: クライアントへ取得した意味を送信する

    各イテレーションのゴール

    1. プログラムから辞書にアクセスし、意味を取得する。(アプリケーション層)
    2. ブラウザからの操作を可能にする。(プレゼンテーション層)
    3. ユーザごとの履歴を残す。(パーシステント層)
    4. Herokuへのデプロイ及び性能テスト(運用)

    第1イテレーション実装方針

    • 辞書はDictionary.com のWeb APIを利用する
    • clj-httpを用いDictionary.comにアクセスし、意味を含むXMLを取得する
    • clj-xpathを用いXMLから意味を取得する
    • clj-jsonを用いClojureオブジェクトをjson文字列に変換する

    異常系の処理(全体方針)

    • 例外発生時には、Exceptionを補足して、アプリケーション例外を投げる
    • tools.loggingを用い、適切にログを残す(例外内容のトレース等)
    その他、機能毎に以下のような異常の発生が考えられます。
    • 設定ファイルの不備
    • 外部APIのダウン
    • 外部APIの仕様変更

    開発環境の整備

    以下のサイトを参考にさせて頂きました。
    • Emacsインストール Link
    • swank-clojureインストール via ELPA Link
    • Leiningenインストール Link

    ビジネスロジックの実装(1)

    本日は以下のコードを紹介します。
    • プロジェクトファイル
    • アプリケーション例外クラス
    • nsマクロ
    • 例外処理関数
    • 設定ファイル読み込み関数

    プロジェクトファイル

    (defproject my-dictionary "0.1.0-SNAPSHOT"
    :description "A tiny web application for requesting Dictionary.com web api"
    :url "http://"
    :license {:name "Eclipse Public License"
    :url "http://www.eclipse.org/legal/epl-v10.html"}
    :dependencies [
    [org.clojure/clojure "1.3.0"]
    [org.clojure/tools.logging "0.2.3"]
    [log4j/log4j "1.2.16"]
    [log4j/apache-log4j-extras "1.1"]
    [clj-http "0.4.3"]
    [clj-json "0.5.0"]
    [clj-xpath "1.3.0"]])
    view raw project.clj hosted with ❤ by GitHub

    Leiningenでプロジェクトを作成した際、自動生成されるproject.cljに
    利用する各種ライブラリを追記します。
    Leiningenは、JavaでいうところのMavenのようなもので、ライブラリの依存関係を解消します。
    プロジェクトファイルはMavenのPOMファイルにあたり、
    lein deps
    を実行することで、依存ライブラリのダウンロード、クラスパスへの追加をすることができます。

    アプリケーション例外クラス

    (ns my-dictionary.AppException
    (:gen-class
    :name myDictionary.java.AppException
    :extends java.lang.RuntimeException))

    例外発生時にはJavaのクラス情報を利用するため、java.lang.RuntimeExceptionを継承した、
    アプリケーション例外クラスを作成します。
    compile関数でこのファイルをコンパイルした後は、他のJavaクラスと同様、
    Clojureプログラム内で利用することができます。

    nsマクロ

    (ns my-dictionary.core
    (:require
    :reload-all
    [clojure.tools.logging :as logging]
    [clj-http.client :as http-client]
    [clj-xpath.core :as xpath]
    [clj-json.core :as json])
    (:import
    (java.lang.RuntimeException)
    (myDictionary.java.AppException)
    (java.io.IOException)
    (org.xml.sax.SAXException)))
    view raw core.clj hosted with ❤ by GitHub

    nsマクロはJavaのpackageとimportを合わせたようなもので、
    名前空間と、その名前空間内で使用するClojure/Javaクラスを宣言します。
    :reload-allオプションは、逐次関連するファイルを再読み込みすることの指定です。
    :requireで宣言されたClojureファイルについては、
    asで付けられたエイリアス/メンバ
    で、指定されたファイルのメンバにアクセスすることができます。

    例外処理関数

    (defn handle-exception
    "to throw application exception"
    [e]
    (do (logging/error (.getMessage e))
    (throw (myDictionary.java.AppException. (.getMessage e) (.getCause e)))))
    view raw core.clj hosted with ❤ by GitHub

    例外の内容をログに記録し、アプリケーション例外を発生させます。
    • do特殊形式: 副作用のある処理を行う
    • clojure.tools.logging/error関数: エラーをログ出力する
    • throw特殊形式: 例外を発生させる

    設定ファイル読み込み関数

    (defn load-properties
    "to load properties from external file"
    [file-path]
    (read-string (try
    (slurp file-path)
    (catch java.io.IOException ioe (handle-exception ioe)))))
    (def property-file-path "private/properties.s")
    ;;; cache properties
    (def properties (get-properties property-file-path))
    view raw core.clj hosted with ❤ by GitHub

    設定ファイルを読み込み、読み込まれた文字列をClojureのオブジェクトに変換します。
    • slurp関数: ファイルの記述内容を文字列として読み込む
    • read-string関数: 文字列をClojureのオブジェクトに変換する
    • try特殊形式: 例外が発生される可能性のある処理を包む。例外発生時に対応する例外を含むcatch節を評価する
    逐次ファイルI/Oが発生することを避けるために、一度だけget-properties関数をよび、
    その値をpropertiesとしてモジュール内に保持しておきます。

    今回の作業は以上。最後までお読み頂きありがとうございました。
    たなけん (作業時間60分(各種インストールは除く))

    2012年7月4日水曜日

    ブログはじめました


    はじめに

    おはようございます。当ブログにアクセス頂き、ありがとうございます。
    最近『今やる、すぐやる、出来るまでやる』がモットーの、たなけんです。
    本ブログは、私の関心のある技術を実際に利用した際の備忘を兼ねた記録となります。
    本エントリでは、Clojureを用いたWebアプリケーション開発の動機と、構想を記載いたします。開発の詳細は本日以降書き綴っていきたいと思います。


    動機

    留学生活も残り半年となり、職場復帰もいよいよとなってきました。そこで、復職後直ちに第一線で働けるよう、リハビリも兼ねていくつかソフトウェアを開発し、その過程をブログに記録していこうと思います。
    これまで蓄積してきたスキルの棚卸しと、新しいテクノロジーのキャッチアップを目的に一歩一歩進めて行きたいと思います。
    第一弾として、英単語学習ウェブアプリケーションを作ってみたいと思います。


    構想

    さて、実際に開発に入る前に、開発プロセスおよび採用技術を整理しておきましょう。
    開発プロセスとしては、以下のようなイテレーション型開発を採用しようと思います。
    1週間で1サイクル回しながら4週間で一応の完成を目指したいと思います。

    1. 構想
    2. 仕様策定
    3. 設計
    4. 実装
    5. テスト
    → 2. に戻って機能追加

    採用技術としては、以下を想定しています。ただし、使いながら適宜変更するかもしれません。

    実行系

    • プレゼンテーション(ユーザインタフェース)層: Clojurescript / Google Closure Tools
    • アプリケーション(ビジネスロジック)層: Compojure & 外部ライブラリ(Loggingなど)
    • パーシステント(トランザクション)層: CongoMongo
    • Web & AP コンテナ: Jetty
    • DB: Mongodb
    • OS: Linux
    • 稼働インフラ(ハードウェア&ファシリティ): Heroku

    開発系
    • 設計: 紙とペン
    • エディタ: clojure-mode / Emacs
    • REPL: swank-clojure / Leiningen
    • バージョン管理: git / Github
    • テスト: clojure.test / xunit / Jasmine / PhantomJS / Selenium
    • タスク(ビルド)自動化: Jenkins

    今回の作業は以上。最後までお読み頂きありがとうございました。
    次回は開発環境の構築と第1イテレーションの仕様策定、実装を予定しています。

    たなけん (作業時間30分)