これはG* Advent Calendar 2015の4日目の記事です。
おさらい
さて、昨日の記事でRatpackでHelloworldを出力出来ました。
今回は、HTTPルーティング(URLと実行されるロジックの組み合わせ)を見て行きましょう。
現在、src/ratpack/ratpack.groovyは以下のようになっているはずです。
import static ratpack.groovy.Groovy.ratpack
ratpack {
    handlers {
        get {
            render "Hello Ratpack!! Groovy's Version: ${GroovySystem.version}"
        }
    }
}
新しいHTTPルーティングルールを追加する
では、現在時刻を表示する簡単なロジックを追加してみます。
import static ratpack.groovy.Groovy.ratpack
ratpack {
    handlers {
        get {
            render "Hello Ratpack!! Groovy's Version: ${GroovySystem.version}"
        }
        // コレを追加。
        get("now") {
            render "${new Date()}"
        }
    }
}
コレでOKです。
ratpackが起動している場合は、http://localhost:5050/nowにアクセスしてみましょう。
時間が表示されるはずです。起動していない場合はプロジェクトトップでgradle run -tを実行してRatpackを起動してください。
(詳細は昨日の記事をご覧ください。)
このように、getメソッドに引数として文字列を渡すと、その文字列自体がURLになります。
とってもシンプル!(今回の場合は("now")の部分)
ちなみに、render部分はrender((new Date()).toString())でもOKです。
URLの一部を動的なものに変更する
この言い方が正しいかどうかわかりませんが、正規表現のように、URLの一部を動的なものに変更できます。
例えば、/user/1の場合、ユーザIDが1のユーザの情報を表示、/user/2の場合、ユーザIDが2のユーザの情報を表示すると言ったことができるようになります。
方法は簡単で、:変数名という書式でURLを指定するだけです。実際に試してみましょう。
import static ratpack.groovy.Groovy.ratpack
ratpack {
    handlers {
        get("user/:userId"){
            String userId = it.getPathTokens().get("userId")
            render "Hello! UserID: ${userId}"
        }
    }
}
さぁ、コレでOKです!
それぞれhttp://localhost:5050/user/0、http://localhost:5050/user/1、http://localhost:5050/user/2と言った形でアクセスしてみましょう。
ちゃんとURLが変わっても上記のメソッドにたどり着いて処理されていますね!
getメソッドで指定したパスに、今回のような:変数名という特殊な記述が遭った場合、it.getPathTokens().get("メソッドで指定した:変数名")でその値を取得できます。
POSTを受け取る
さて、今までブラウザでアクセスして簡単に動作を確認できました。
しかし、フォームから値を送信したりする場合にはPOSTが用いられることもあります。
それでは先ほどのhttp://localhost:5050/user/0にPOSTでアクセスしてみましょう。
POSTのテストでお馴染みのcurlコマンドを使ってコンソールからテストします。
[koji:advent2015]$ curl -X POST http://localhost:5050/user/0
Client error 405% 
あれ?HTTPステータス405が返されました。405は許可されていないメソッド、という意味です。
では、先ほどのソースを、POSTで受け入れられるように変更しましょう!
import static ratpack.groovy.Groovy.ratpack
ratpack {
    handlers {
        // getをpostに書き換える。
        post("user/:userId"){
            String userId = it.getPathTokens().get("userId")
            render "Hello! UserID: ${userId}"
        }
    }
}
なんとコレだけです!
では再度テストしてみましょう。
[koji:advent2015]$ curl -X POST http://localhost:5050/user/0
Hello! UserID: 0%
動きました!
Ratpkackでは、handlersの中に記述するメソッドが、HTTPメソッドに対応します。
なので、get("...")の場合はGETメソッド、post("...")の場合はPOSTメソッドの際に実行されることになります。
すべてのHTTPメソッドを扱う
さて、先程の問題で実は問題が発生しています。
ブラウザで再度http://localhost:5050/user/0にアクセスしてみてください。
Ratpackのエラー(ステータスが405)が表示されますね。。。
コレは当然、メソッドをgetからpostに書き換えたためです。
しかしコレだとGETでもPOSTでもOK、同じ情報を見せたい、と言った場合に対応できなくなってしまいます。
そのためRatpackではpathと呼ばれる特別なメソッドを用意してくれています。
import static ratpack.groovy.Groovy.ratpack
ratpack {
    handlers {
        // getでもpostでもなく、pathを指定
        path("user/:userId"){
            String userId = it.getPathTokens().get("userId")
            render "Hello! UserID: ${userId}"
        }
    }
}
これで、このルーティングルール、はどんなHTTPメソッドでも対応できるようになりました。
実際に試してみましょう。
[koji:advent2015]$ curl -X POST http://localhost:5050/user/0
Hello! UserID: 0%
[koji:advent2015]$ curl -X GET http://localhost:5050/user/0
Hello! UserID: 0%
[koji:advent2015]$ curl -X PUT http://localhost:5050/user/0
Hello! UserID: 0%
[koji:advent2015]$ curl -X DELETE http://localhost:5050/user/0
Hello! UserID: 0%
せっかくなので、POST、GET、PUT、DELETEの基本的な4つのHTTPメソッドを試してみました。
すべて問題なく扱われていますね!
HTTPメソッドごとに処理を切り替える
さて、一つのルーティングルールでどんなHTTPメソッドでも受け入れられるようになりましたが、コレだとちょっとまずいですね。
というのも、RESTなAPIでよくある、同じURLでも、GETならデータ表示、DELETEならデータ削除、というような、同一URLでもHTTPメソッドが変われば処理を切り替える、ということが出来ないのです。
しかしご安心を!当然、Ratpackでも対応できます!
そのためにはbyMethodという機能を利用します。
import static ratpack.groovy.Groovy.ratpack
ratpack {
    handlers {
        path("user/:userId"){
            String userId = it.getPathTokens().get("userId")
            byMethod {
                get {render "Hello! UserID: ${userId} on GET"}
                post {render "Hello! UserID: ${userId} on POST"}
                put {render "Hello! UserID: ${userId} on PUT"}
                delete {render "Hello! UserID: ${userId} on DELETE"}
            }
        }
    }
}
byMethodの中にさらに、各HTTPメソッド事の処理を書いてあげるだけです。
試してみましょう。
[koji:advent2015]$ curl -X GET http://localhost:5050/user/0
Hello! UserID: 0 on GET%
[koji:advent2015]$ curl -X POST http://localhost:5050/user/0
Hello! UserID: 0 on POST%
[koji:advent2015]$ curl -X PUT http://localhost:5050/user/0
Hello! UserID: 0 on PUT%
[koji:advent2015]$ curl -X DELETE http://localhost:5050/user/0
Hello! UserID: 0 on DELETE%
同じURLで、HTTPメソッドごとにちゃんと切り替えられました!
階層の深いURLを整理する
さて、以下のサンプルを見てみてください。
import static ratpack.groovy.Groovy.ratpack
ratpack {
    handlers {
        get("a/b/user") {
            render "hello. here is /a/b/user"
        }
        get("a/b/c/user") {
            render "hello. here is /a/b/c/user"
        }
    }
}
/a/bとうURL配下に/userと/c/userという2つのURLがぶら下がっています。
この2件だけならまぁ大丈夫でしょうが、もしURLの数が増えた場合に、/a/bが/1/2に変更しなければならない要件が出来たば場合を考えるとちょっと不味そうですね。
そこで、prefixというこれまた特種な機能を使ってルーティングを整理することが出来ます。
import static ratpack.groovy.Groovy.ratpack
ratpack {
    handlers {
        prefix("a") {
            prefix("b") {
                prefix("user") {
                    get { render "hello. here is /a/b/user" }
                }
                prefix("c") {
                    prefix("user") {
                        get { render "hello. here is /a/b/c/user" }
                    }
                }
            }
        }
    }
}
動作は全く同じです。
これで、例えばaが1に変わった場合、1箇所変更するだけで両方のgetメソッドへのURLが同時に変更されます。
URLからクエリストリングを取得する
アクセスされた際にURLにクエリストリング(?key1=value1&key2=value2って感じのあれ)が付いている際に、当然そのクエリストリングを取得することが出来ます。
また、POSTアクセスであってもURLにクエリストリングがあれば取得することが可能です。
取得方法は簡単で、request.queryParams.クエリストリングのパラメタ名とするだけです。
では、pathを使って試してみましょう。
import static ratpack.groovy.Groovy.ratpack
ratpack {
    handlers {
        path("query") {
            render request.queryParams.id
        }
    }
}
試してみます。
[koji:advent2015]$ curl -X POST http://localhost:5050/query?id=hoge
hoge%
[koji:advent2015]$ curl -X GET http://localhost:5050/query?id=hoge
hoge%
動いていますね!
POSTされたデータを取得する。
さて、クエリストリングが取得できたら当然POSTされたデータも取得してみましょう!
いきなりコードです。
mport static ratpack.groovy.Groovy.ratpack
import ratpack.exec.Promise
import ratpack.form.Form
ratpack {
    handlers {
        post("post-data") {
            Promise<Form> form = it.parse(Form.class)
            form.then {
                String name = it.name // it.get("name")と書いてもOK
                String samePiyos = (it.getAll("piyo").join(","))
                render "name:${name}, piyos: ${samePiyos}"
            }
        }
    }
}
import分も増えているし、なんだか雰囲気が変わりましたね?
実は自分もまだよくわかっていません。。。このコードの重要なキーワードとしてPromiseがあります。
ここを勉強すればもっとよく分かるのでは。。。
その部分はまた別の機会で投稿したいと思います。
では実際にテストしてみましょう。
[koji:advent2015]$ curl -X POST http://localhost:5050/post-data -d "name=koji" -d "piyo=piyo1" -d "piyo=piyo2"
name:koji, piyos: piyo1,piyo2% 
動いていますね。プロミスなフォームの中で、it.nameのように、パラメタ名を指定するだけで値が取得できます。
piyoというパラメタ名が複数存在しています。HTMLフォームのチェックボックスとかがこんな感じになりますよね。
その場合は、it.getAll("パラメタ名")とすれば、リストに同じパラメタ名を持つ値が格納された返されます。
あとはそれらを煮るなり焼くなり。。。
まとめ
さて、どうだったでしょうか。
ざっと流し読みするだけど「そうそうそうだよね〜」って感じになるくらい素直な仕組みではないでしょうか。
まぁRatpackのドキュメントからこれらを読み取ることが出来なくて私は結構苦労しましたが。。。
WEBフレームワークなので当然テンプレートを使ってHTMLを返すことも出来ますが、とりあえず今回の内容を押さえれば基本的なAPIサーバは簡単に構築する足がかりになると思います。
「おいおい、APIサーバだなんて偉そうに言うならJSONくらい教えろよ」という話ですね。コレはまた明日!