ラベル Google App Engine の投稿を表示しています。 すべての投稿を表示
ラベル Google App Engine の投稿を表示しています。 すべての投稿を表示

2013年12月7日土曜日

Skeleton Project for Google App Engine/Go + Dart

2016/4/30更新

この記事の内容はすでに古く、またGithub上にリポジトリにおいてあるコードも最新のDartに合わせて変わっているため参考にならない点に注意。

Dartも1.0になったことだしそろそろ試す人が増えてきてもいいはず。ということで、Google App Engine上でDartなウェブアプリを作りたい人のためにスケルトンのプロジェクトを作った。バックエンドの言語はGoでフロントエンドはDart。動作確認はDartiumでしかしてないので、他のブラウザで動かしたい人はどっかでdart2js通せばいける…はず? 正直dart2jsほとんど試したことないからよくわからない。

スケルトンプロジェクトについて

Github上にリポジトリを作ったのでそれをCloneするなりForkするなり好きにやってもらえればいいはず。

ちなみに今回は実際にApp Engine上にデプロイしてあるので、そっちでどんなもんか確認することもできる。URLはgae-go-dart-skeleton.appspot.comで、Dartiumでアクセスすればちゃんと見れるはず。なおローカルからDartium上で動かすとこんな感じ(Windows 8.1 + GAE/Go 1.8.8 + Dart SDK 1.0.0.3_r30188)。

スケルトン

スケルトンプロジェクトの動かし方

gae-go-dart-skeletonを動かすには以下の手順を踏む必要がある。

  1. GAE/GoのSDKとDartのSDKをダウンロードしてきてパスを通す
  2. git clone git@github.com:rillomas/gae-go-dart-skeleton.gitする。あるいは自分のとこにForkしてそっちをcloneする
  3. pub getして必要なパッケージを落としてくる
  4. dev_appserver.pyを使ってローカルで動かす
  5. (デプロイする場合は)appcfg.pyでアプリをアップする

pub get

pubはDartに標準でついてくるパッケージマネージャで、pubspec.yamlファイルに必要なものを書くと落としてきてくれる。今回のpubspec.yamlはこんな感じ。

dependencies:
  logging: any
  polymer: any

Polymerとログ用のパッケージしか書いてないけど、それ以外に必要なパッケージはpub側で判断してもってきてくれるからこれで十分。でpubspec.yamlが置いてあるディレクトリでpub getするとこんな感じに実行される。

$ pub get
Resolving dependencies............................
Downloading polymer 0.9.1+1 from hosted...
Downloading polymer_expressions 0.9.1 from hosted...
Downloading shadow_dom 0.9.1 from hosted...
Downloading observe 0.9.1+1 from hosted...
Downloading html5lib 0.9.1 from hosted...
Got dependencies!

ルートディレクトリにpackagesってディレクトリが作られて、そこにパッケージがすべてダウンロードされてくるはず。あとは必要なところにpackagesへのリンクが自動的に張られる。それ以外にもpubspec.lockていうファイルが作られるけど、それはpubがバージョンの管理用に自動生成するファイルなのであまり気にしなくて大丈夫。

App Engine関係

App Engine関係は公式ドキュメントがあるからそっちでよろしく。dev_appserver.pyまわりはここ、appcfg.pyはこことか。

コードについて

今回のはPolymerを使った極めて単純なウェブアプリなんだけど、クライアントサイドで完結するようなアプリだと面白くないのでサーバー側のREST APIを叩いてdatastoreからデータを持ってきてそれを表示している。

サーバーサイド

サーバー側は来訪者の情報を提供するためのvisitorInfo APIを用意している。パスはapi/1/visitorInfoで、そこにGETをかければjsonが返ってくるし、POSTすればサーバー側の数値を書き換えることができる。実際アップしてあるアプリのAPIにアクセスすると現在の来訪者数累計が取得できるはず。やっている処理の内容はserver/main.goの以下のあたり。

func init() {
    http.HandleFunc("/api/1/visitorInfo", handleVisitorInfoRequest)
}

func handleVisitorInfoRequest(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    c.Debugf("Method: %s Url:%s ContentLength: %d\n", r.Method, r.URL, r.ContentLength)
    if (r.Method == "GET") {
        getVisitorInfo(c,w,r)
    } else if (r.Method == "POST") {
        setVisitorInfo(c,w,r)
    }
}

getVisitorInfo()の中でdatastoreからデータの取得とGETへの応答、setVisitorInfo()のなかでPOSTで送られてきたデータのdatastoreへの格納をやっている。datastoreへのget/setは固定の文字列キーでやってるので基本的に間違いはないはずなんだけど、初回にまだ何もsetされる前にgetが走る可能性があるのでNoSuchEntityエラーだけは現状握りつぶしてる。

const (
    visitorInfoKind string = "visitorInfo"
    visitorInfoKey string = "theVisitorInfoKey"
)

type VisitorInfo struct {
    Count int // total number of visitors
}

func getVisitorInfo(c appengine.Context, w http.ResponseWriter, r *http.Request) {
    k := datastore.NewKey(c,visitorInfoKind, visitorInfoKey, 0, nil)
    info := new(VisitorInfo)
    err := datastore.Get(c,k, info)
    // ignore the NoSuchEntityError and return the default value if we don't have the entity stored yet
    if err != nil && err != datastore.ErrNoSuchEntity {
        c.Errorf("%s", err.Error())
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    outBuf, err := json.Marshal(info)
    if err != nil {
        c.Errorf("%s", err.Error())
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    size, err := w.Write(outBuf)
    if err != nil {
        c.Errorf("%s", err.Error())
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    c.Debugf("Wrote %d bytes as response\n", size)

}

func setVisitorInfo(c appengine.Context, w http.ResponseWriter, r *http.Request) {
    buf, err := ioutil.ReadAll(r.Body)
    if err != nil {
        c.Errorf("%s", err.Error())
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    var info VisitorInfo
    json.Unmarshal(buf, &info)
    k := datastore.NewKey(c,visitorInfoKind, visitorInfoKey, 0, nil)

    _,err = datastore.Put(c,k,&info)
    if err != nil {
        c.Errorf("%s", err.Error())
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
}

クライアントサイド

クライアント側ではMainApp(index.htmlから参照されてるWeb Component)が表示された段階でサーバーに対してGETを発行して、結果をもとに自身の内容を更新している。コード的にはclient/lib/components/main_app.dartの以下のあたり。

    @override
    void enteredView() {
        super.enteredView();

        var domain = ServerChannel.generateRootDomain(window.location);
        ServerChannel.getVisitorInfo(domain).then((info) {
            info.count++;
            visitorInfo = info;

            // update the visitor info on the server side
            ServerChannel.sendVisitorInfo(info, domain);
        });
    }

enteredView()はWeb Component側がdocumentに挿入されたときに呼ばれてくる関数で、今回はそこでvisitorInfo APIへGETを投げている。サーバーから今までの来訪者数を取得して、自分の分を加算して表示、更にサーバー側の来訪者数をPOSTで更新するところまで通してやっている。

ちなみにこのやり方だと来訪者数が一瞬だけゼロ表示されてしまうので、情報がもらえるまでは表示そのものを隠すとかした方が美しいのかも。めんどうだからやってない。

実際にHttpリクエストを投げる部分はすべてServerChannelというクラスで隠蔽している。コード的にはclient/lib/services/server_channel.dart

class ServerChannel {

    static Future<VisitorInfo> getVisitorInfo(String domain) {
        var url = "${domain}${visitorInfoApiPath}";
        Logger.root.info("getting from ${url}");
        var task = new AsyncGet<int>();
        var f = task.request(url, (res) {
            Logger.root.info("response: $res");
            int count = -1;
            if (res.isEmpty) {
                Logger.root.warning("received empty string");
                return count;
            }
            Map map = JSON.decode(res);
            var info = new VisitorInfo.fromJson(map);
            return info;
        });
        return f;
    }

    static void sendVisitorInfo(VisitorInfo info, String domain) {
        var request = new HttpRequest();
        var url = "${domain}${visitorInfoApiPath}";
        Logger.root.info("Sending info to ${url}");
        request.open("POST", url);
        var str = JSON.encode(info);
        request.send(str);
    }

    // Generate the root domain from location information
    static String generateRootDomain(Location location) {
        return "${location.protocol}//${location.host}/";
    }

    static const string visitorInfoApiPath = "api/1/visitorInfo";
}

内部で使っているAsynGetていう独自クラスがFutureやらCompleterやらを駆使して非同期なコードをDartっぽくなるようにラップしてる。

class AsyncGet<Output> {
    Future<Output> request(String url, Output process(String response)) {
        HttpRequest.request(url).then((r) {
            if (r.readyState == HttpRequest.DONE &&
                (r.status == 200)) {
                Output out = process(r.responseText); // convert the raw response text
                _completer.complete(out);
            }
        });
        return _completer.future;
    }

    Completer<Output> _completer = new Completer<Output>();
}

ServerChannelを経由してサーバーから取得できた来訪者情報は、visitorInfo変数に代入された段階で自動的にバインド先に反映される。これはPolymerのおかげ。その辺のバインドが書いてあるのはMainAppのビューに相当するclient/lib/components/main_app.html。

<polymer-element name="main-app">
    <template>
        <h2>Welcome to my Dart Skeleton</h2>
        <p>You are the #{{visitorInfo.count}} visitor!</p>
    </template>
    <script type="application/dart" src="main-app.dart"></script>
</polymer-element>

自分でDOMいじる必要がないから楽。

雑感

App Engine + Dartでもわりと普通に動いてる印象。ただこっちで試したところ、Channel APIをjs-interopでラップするとデプロイしたときに"Uncaught Closure call with mismatched arguments: function 'call'"とかいう謎のエラーでうまく動かないことがわかってる。でもなぜかローカルの環境だとちゃんと動く。

DartにPolymerが無くてWeb UIだった頃から追ってるけど、Polymerは素直に書けてちゃんと動くしいいと思う。Web UIの頃はところどころ未完成でいまいちだった。個人的には@observableとかのアノテーションを書くのはちょっと違和感があるんだけど、たぶん慣れの問題。あとは早めにWeb AnimationsとPolymer UI Elementsを移植してもらえると助かるかな。

そんな感じ。Dart VMはいずれChrome stableにマージされる予定らしいし、dart2jsの性能もメリメリ向上してるのでみんなどんどんDart書くといいよ。

Dart Advent Calendar 2013 12月7日担当

2013年10月20日日曜日

Creating a custom class with an event stream

Dartでウェブアプリを作る際、ある自作のクラスから何らかのイベントを飛ばしたいときがある。そのクラスが自作のWeb Componentの場合、Polymer.dartの@observableやら**Changedイベント関数を定義すればだいたいそれでどうにかなる。というか実際いまのところそれでなんとかなってる。

問題になってくるのはUIの直接絡まないクラスでイベントを定義したい場合で、そのような場合はどうすればいいのかいまいちよくわからなかった。C#で言うところのeventを定義してそのリスナーを複数登録したいだけなんだけどどうすりゃいいの、ていう状態。

その辺を改めて調べて、だいたいのやり方が把握できた気がするのでここに記しておく。なお、試したバージョンはDart SDKの0.8.1.2_r28355とGoogle App Engine(Go) の1.8.6.1。

Channel APIをラップする

今回実際に必要になったので作ったのがGoogle App Engine Channel APIのクライアントサイドJavascriptをラップするクラス。Google App EngineのChannel APIは今のところJavascript用のクライアントサイドスクリプトしか用意してないため、Dartから使おうとするとjs-interopあたりを駆使して自分でラップする必要がある。たしか非公式なパッケージもどっかにあったけど、どうせそんなアップデートされないだろうし公式にDartがサポートされるまでは自分でラップした方が確実だろう、ということで自分でやった。

Server Channel

下のServerChannelがChannel API経由のメッセージ受信、及びサーバーへのリクエスト送信を担当するクラス。Web Socketと違ってChannel APIは片方向なので、サーバーへのリクエストは都度HTTPで送る必要がある。サーバーへのインタフェースは一箇所に集めたかったからそのリクエストを出す部分もこいつに担当させてる。クラス内で使われてるClientとUserは単なるjsonをクラスにマッピングしたものと考えてもらえればOK(つまりそのクラス自体には現状何も機能が無い)。

import 'dart:async';
import 'dart:html';
import 'dart:json' as json;
import 'package:js/js.dart' as js;

/**
 * Interface class to the server
 */
class ServerChannel {

    ServerChannel(this.socket, this.client, this.user);

    Client client;
    Object socket;
    User user;

    static Future<ServerChannel> connect(String domain) {
        var request = new HttpRequest();
        var url = "${domain}${clientApiPath}";
        print("sending to ${url}");
        request.open("POST", url);
        var str = json.stringify(new Client(-1,"",""));
        print(str);

        ServerChannel chan = new ServerChannel(null, null, null);
        chan._controller = new StreamController<Object>.broadcast();
        // handler for the server response
        request.onReadyStateChange.listen((_) {
            if (request.readyState == HttpRequest.DONE &&
                (request.status == 200 || request.status == 0)) {
                print(request.responseText);
                Map map = json.parse(request.responseText);
                var client = new Client.fromJson(map["Client"]);
                var user = new User.fromJson(map["User"]);

                // connect to the server via channel api
                var appengine = js.context.goog.appengine;
                var channel = new js.Proxy(appengine.Channel, client.channelToken);
                var socket = channel.open();

                // setup handlers for the channel
                socket.onopen = new js.Callback.once(() {
                    print("channel opened");
                });

                socket.onmessage = new js.Callback.many(chan._onMessage);

                socket.onerror = new js.Callback.many(chan._onError);

                socket.onclose = new js.Callback.once(() {
                    print("channel closed");

                    // disopose Callback.many explicitly
                    socket.onmessage.dispose();
                    socket.onerror.dispose();
                });

                chan.socket = socket;
                chan.client = client;
                chan.user = user;
                chan._completer.complete(chan);
            }
        });
        request.send(str);
        return chan._completer.future;
    }

    void listen(Function onMessage) {
        _controller.stream.listen(onMessage);
    }

    void _onMessage(Object message) {
        print(message);
        // send a meesage to all the listeners
        // We need to pass the actual data in order to avoid
        // "Proxy has been invalidated" error
        _controller.add(message.data);

    }

    void _onError(Object message) {
        print(message);
        // send a message to all the listeners
        // _controller.addError(message);
    }

    void sendRemark(Remark remark, String domain) {
        var request = new HttpRequest();
        var url = "${domain}${remarkApiPath}";
        print("sending to ${url}");
        request.open("POST", url);
        var str = json.stringify(remark);
        print(str);
        request.send(str);
    }


    static const string remarkApiPath = "api/1/remark";
    static const string clientApiPath = "api/1/client";
    Completer<ServerChannel> _completer = new Completer<ServerChannel>();
    StreamController<Object> _controller;
}

ServerChannelだけだとわかりづらいので、それを実際に使ってる部分のコードも載せる。 中でやってることは以下の通り。

  1. 指定されたドメインに対してChannel APIの接続リクエストを送る
  2. 接続が成功したらServerChannelのインスタンスに対してメッセージ受信時のハンドラ(_onMessage)を登録する
class SampleClient {
    void init() {
        // connect to server
        print("Sending connect request");
        var domain = window.location.href;
        ServerChannel.connect(domain).then((chan) {
            _channel = chan;
            _channel.listen(_onMessage);
        });
    }

    void _onMessage(Object message) {
        print("onmessage at channel: ${message}");
    }

    ServerChannel _channel;
}

Stream Controller

先ほどのServerChannelでイベントを扱っているキモの部分はStreamControllerで、それ以外はあまり関係無い。StreamControllerの詳細はドキュメントの方を読んでもらうとして、簡単に言うとStreamControllerはDartのイベントまわりのインフラであるStreamを叩くための補助クラス。イベントの下回りはStream側で担当してくれるので、Streamを簡単に任意のものと組み合わせられるようにしてくれるのがStreamController。

ServerChannel.connect関数

実際にStreamController経由でイベントを飛ばすにはまずサーバーからChannel API経由でメッセージを受け取れる必要があって、その辺の準備をやっているのがconnect関数。connect関数はだいたい以下の流れになってる。

  1. ServerChannelクラスを初期化する。中のStreamControllerは複数のリスナーを想定してるのでbroadcast関数で複数リスナー用のモードにする。
  2. 指定されたドメインに対してクライアント情報を送る。上のコードではCompleterを使ってFutureを返しているので、connect関数はすぐに抜けて実際にリクエストを送って返答を待つ部分は非同期に実行される。
  3. サーバーのレスポンスを待って、レスポンスからChannel接続用のクライアントIDとその他ユーザー情報を取得する
  4. js-interopを使ってChannel APIのJavascriptコードを呼び出す。ここではChannel APIのソケットを初期化して内部のハンドラを登録してる。
  5. 完成したServerChannelインスタンスをFutureの先で待ってるコードに渡す

js-interopの落とし穴

上の手順を踏むとメッセージが来るたびにServerChannel内部の_onMessageが呼ばれるようになって、あとはそれを外部のリスナーへ通知してやればおしまい。それをやってるのがStreamControllerのadd関数なんだけどここで一点注意が必要。

どうもjs-interopから戻ってきたオブジェクトそのものへの参照は保持してはダメらしく、保持しようとすると"Exception: Proxy js-ref-8 has been invalidated. "みたいなエラーが飛ぶ。そこで上のコードではオブジェクト本体ではなく、オブジェクトが持つデータをリスナーに渡すことでエラーを回避してる。

まとめというか感想

  • UIの絡まない汎用的なイベントはStreamController経由で飛ばしましょう。UIが絡む場合は素直にPolymerの機構を使いましょう。
  • js-interop使うときはオブジェクトの参照に注意。それさえ気をつければJavascriptのライブラリもDartから割と普通に使えるかも。

2011年11月12日土曜日

Setting up Google App Engine Environment (Go) for Ubuntu 11.10

Ubuntu 11.10でGoogle App Engine (Go)の環境をセットアップするのに手間取ったのでメモ。

Google App Engine SDKの取得

いまの最新版SDK1.6.0を落としてセットアップ。パスも適当に通す。

$ cd ~/lib
$ wget http://googleappengine.googlecode.com/files/go_appengine_sdk_linux_386-1.6.0.zip
$ unzip go_appengine_sdk_linux_386-1.6.0.zip
$ PATH=$PATH:~/lib/google_appengine

Python 2.5のセットアップ

Google App Engineの開発用ローカルサーバー(dev_appserver.py)を正常に動作させるにはまだPython 2.5が必要なため、インストールする。ただUbuntu 11.10はもうPython 2.5を切りすててしまっているため、外部のリポジトリから入手する必要がある。

$ sudo add-apt-repository ppa:fkrull/deadsnakes
$ sudo apt-get update
$ sudo apt-get install python2.5 python2.5-dev

PILのセットアップ

PILがインストールされていないとdev_appserver.pyの起動時に"Could not initialize images API; you are likely missing the Python "PIL" module."というエラーが出るため、PILもインストールする。

$ wget http://effbot.org/downloads/Imaging-1.1.7.tar.gz 
$ tar xzf Imaging-1.1.7.tar.gz 
$ cd Imaging-1.1.7/
$ sudo python2.5 setup.py install

これであとは下のような感じでdev_appserver.pyを起動すればいける。

$ python2.5 ~/lib/google_appengine/dev_appserver.py server/ 

Python 2.7でやろうとすると

そもそもなんでこんなことを今さらやったかというと、Python 2.7でdev_appserver.pyを実行すると、ときたま落ちるから。試しにChannel APIを使おうとしたら↓のエラーで死ぬようになってしまったため、やはり正規のPython 2.5を入れる必要がでた、ということ。

INFO     2011-11-12 14:29:41,093 dev_appserver.py:2753] "POST /_ah/channel/connected/ HTTP/1.1" 404 -
----------------------------------------
Exception happened during processing of request from ('0.1.0.10', 80)
Traceback (most recent call last):
  File "/usr/lib/python2.7/SocketServer.py", line 284, in _handle_request_noblock
    self.process_request(request, client_address)
  File "/usr/lib/python2.7/SocketServer.py", line 311, in process_request
    self.shutdown_request(request)
  File "/usr/lib/python2.7/SocketServer.py", line 459, in shutdown_request
    request.shutdown(socket.SHUT_WR)
AttributeError: 'ChannelPresenceConnection' object has no attribute 'shutdown'
----------------------------------------
ERROR    2011-11-12 14:29:41,128 dev_appserver_main.py:664] Error encountered:
Traceback (most recent call last):

  File "/home/masato/lib/google_appengine/google/appengine/tools/dev_appserver_main.py", line 657, in main
    http_server.serve_forever()

  File "/home/masato/lib/google_appengine/google/appengine/tools/dev_appserver.py", line 3527, in serve_forever
    self.handle_request()

  File "/home/masato/lib/google_appengine/google/appengine/tools/dev_appserver.py", line 3490, in handle_request
    self._handle_request_noblock()

  File "/usr/lib/python2.7/SocketServer.py", line 287, in _handle_request_noblock
    self.shutdown_request(request)

  File "/usr/lib/python2.7/SocketServer.py", line 459, in shutdown_request
    request.shutdown(socket.SHUT_WR)

AttributeError: 'ChannelPresenceConnection' object has no attribute 'shutdown'

Now terminating.

2011年8月7日日曜日

Creating an original REST API using Google App Engine and Go (7)

前回でDELETEのAPIができたので、今回は最後になるPUTを実装する。

コメント更新用のAPI

従来の作りを踏襲して以下の形にする。

http://localhost:8080/1/remark/put/[コメントのID]

サーバーサイド

実装してみたところ、PUTが一番めんどうなことになっていた。DELETEのときと一緒で、PUT用のAPIとかが存在しないので、手動で色々やる必要がある。

func init() {
    http.HandleFunc("/", errorHandler(root))
    http.HandleFunc("/1/remark/post/", errorHandler(postRemark))
    http.HandleFunc("/1/remark/get/", errorHandler(getRemark))
    http.HandleFunc("/1/remark/delete/", errorHandler(deleteRemark))
    http.HandleFunc("/1/remark/put/", errorHandler(putRemark))
}
...

// Handle a put request of remarks
func putRemark(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    switch r.Method {
    case PUT:
        c.Infof("Putting Remark\n")
        c.Infof("Request: %s\n", r)
        c.Infof("Requested URL: %s\n", r.RawURL)
        c.Infof("Requested URL path: %s\n", r.URL.Path)

        // check if we got the updated remark
        buf := bytes.NewBuffer(nil);
        _, err := buf.ReadFrom(r.Body)
        if err != nil {
            http.Error(w, err.String(), http.StatusInternalServerError)
            return
        }
        bodyStr := buf.String()
        c.Infof("body: %s\n", bodyStr)
        values, err := http.ParseQuery(bodyStr)
        if err != nil {
            http.Error(w, err.String(), http.StatusInternalServerError)
            return
        }
        remarkString, present := values[REMARK_FIELD]
        if !present {
            msg := "Required field does not exist\n"
            c.Infof(msg)
            http.Error(w, msg, http.StatusBadRequest)
            return
        }

        // Check if we were requested for a specific remark
        id, foundID := getRemarkID(r.URL.Path)
        if !foundID {
            msg := "Specified remark does not exist\n"
            c.Infof(msg)
            http.Error(w, msg, http.StatusNotFound)
            return
        }
        query := datastore.NewQuery(REMARK_KIND).Filter("Time=", id)
        
        // check if any results exist
        count, err := query.Count(c)
        if err != nil {
            http.Error(w, err.String(), http.StatusInternalServerError)
            return
        }
        
        if count == 0 {
            msg := "Specified remark does not exist\n"
            c.Infof(msg)
            http.Error(w, msg, http.StatusNotFound)
            return
        }
        
        // update remark of given id
        itr := query.Run(c)
        for {
            var remark Remark
            key, err := itr.Next(&remark)
            if err == datastore.Done {
                break
            } else if err != nil {
                http.Error(w, err.String(), http.StatusInternalServerError)
                return
            }
            c.Infof("Updating id:%d remark:%s", key.IntID(), remark.Content)

            store := Remark {
                Content : remarkString[0],
                Time : datastore.SecondsToTime(key.IntID()), 
            }

            _, err = datastore.Put(c, key, &store)
            if err != nil {
                http.Error(w, err.String(), http.StatusInternalServerError)
            }
        }
    default:
        msg := "Invalid request\n"
        c.Infof(msg)
        http.Error(w, msg, http.StatusBadRequest)
    }
}

POSTのときと違ってParseForm()が使えないので、自分でBodyを読んでhttp.ParseQuery()を呼んで、という風な手順を踏まないといけくなっている。ただリクエストをパースする部分さえできてしまえば、あとは他のAPIとだいたい同じ。

クライアントサイド

クライアントサイドは今までAPIごとに用意してきたクライアントを一つに統合して、引数で実行を制御するように変更した。

package main

import (
    "http"
    "fmt"
    "os"
    "bytes"
    "flag"
    "strconv"
)

// print the body of the given response
func printResponse(r *http.Response) {
    fmt.Println(r.Status)
    bufSize := 256
    buf := make([]byte, bufSize)
    for {
        read, err := r.Body.Read(buf)
        if read == 0 && err == os.EOF {
            fmt.Println("Finished reading")
            break
        } else if err != nil {
            fmt.Printf("Error during read: %s",err)
            break
        }
        // convert the buffer to a string and print it
        fmt.Println(string(buf[0:read]))
    }
    r.Body.Close()
}


func main() {
    id := flag.Int64("id", 0, "Set the id of the remark")
    method := flag.Int("method", 0, "Set the method to use. GET: 0, POST:1, DELETE:2, PUT:3")
    remark := flag.String("remark", "", "Set the remark string")
    flag.Parse()

    client := new(http.Client)
    remarkUrlRoot := "http://localhost:8080/1/remark/"

    switch (*method) {
    case 0: // GET
        getUrl := remarkUrlRoot + "get"
        r, err := client.Get(getUrl)
        if err != nil {
            fmt.Printf("Error: %s",err)
            return
        }
        printResponse(r)
    case 1: // POST
        postUrl := remarkUrlRoot + "post/"
        data := http.Values { 
            "remark": {*remark,},
        }
        r, err := client.PostForm(postUrl, data)
        if err != nil {
            fmt.Printf("Error: %s",err)
            return
        }
        printResponse(r)
    case 2: // DELETE
        deleteUrl := remarkUrlRoot + "delete/" + strconv.Itoa64(*id)
        fmt.Println(deleteUrl)
        request, err := http.NewRequest("DELETE", deleteUrl, nil)
        if err != nil {
            fmt.Printf("Error: %s",err)
            return
        }
        r, err := client.Do(request)
        if err != nil {
            fmt.Printf("Error: %s",err)
            return
        }
        printResponse(r)
    case 3: // PUT
        putUrl := remarkUrlRoot + "put/" + strconv.Itoa64(*id)
        putBuf := bytes.NewBuffer([]byte("remark="+*remark))
        request, err := http.NewRequest("PUT", putUrl, putBuf)
        if err != nil {
            fmt.Printf("Error: %s",err)
            return
        }
        request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
        request.Header.Add("Content-Length", string(putBuf.Len()))
        r, err := client.Do(request)
        if err != nil {
            fmt.Printf("Error: %s",err)
            return
        }
        printResponse(r)
    default:
        fmt.Println("Unknown method")
        return 
    }
}

PUTのときはPOSTのマネをしてContent-Typeにapplication/x-www-form-urlencodedを設定して送っている。この辺はもっと別のやり方でもいいと思う。

クライアント実行例

$  ./client -h
flag provided but not defined: -h
Usage of ./client:
  -id=0: Set the id of the remark
  -method=0: Set the method to use. GET: 0, POST:1, DELETE:2, PUT:3
  -remark="": Set the remark string

$ ./client
200 OK
id=1312687737000000&remark=asfasdfasdfas
id=1312687714000000&remark=aardvark
id=1312687246000000&remark=aardvark

Finished reading
$ ./client -method=2 -id=1312687737000000
200 OK
Finished reading

$ ./client
200 OK
id=1312687714000000&remark=aardvark
id=1312687246000000&remark=aardvark

$ ./client -method=3 -id=1312687246000000 -remark=thunderbolt
200 OK
Finished reading

$ ./client
200 OK
id=1312687714000000&remark=aardvark
id=1312687246000000&remark=thunderbolt

Finished reading

サーバーサイド出力(PUTしたとき)

2011/08/07 06:28:45 INFO: Putting Remark
2011/08/07 06:28:45 INFO: Requested URL: /1/remark/put/1312687246000000
2011/08/07 06:28:45 INFO: Requested URL path: /1/remark/put/1312687246000000
2011/08/07 06:28:45 INFO: body: remark=thunderbolt
2011/08/07 06:28:45 INFO: Updating id:1312687246 remark:aardvark
INFO     2011-08-07 06:28:45,116 dev_appserver.py:4248] "PUT /1/remark/put/1312687246000000 HTTP/1.1" 200 -

まとめ

ということで数回に渡ってGo + Google App EngineでRESTなAPIの基礎的な部分を試してきた。一番厄介だったのはhttpパッケージまわりで、サンプルコードもあまり無いため探りつつ実行する、という感じだった。逆にGoogle App Engine側はそれほど探ることなく、素直に書いて素直に実行できた気がする。

連載中にGoogle App Engine側もアップデートをして、Go版も色々と進化してきたのでまたおいおい別の記事も書いていきたい。

2011年8月2日火曜日

Creating an original REST API using Google App Engine and Go (6)

前回から引きつづいて、今度はDELETE用のAPIを実装していく。

コメント削除用のAPI

REST的には削除対象はURLで表現する形になるので、今回もそれにならったAPIとする。

http://localhost:8080/1/remark/delete/[コメントのID]

サーバーサイド

API的にはgetに似ているけど、コード自体もかなりgetと似た形になる。

...
func init() {
    http.HandleFunc("/", errorHandler(root))
    http.HandleFunc("/1/remark/post/", errorHandler(postRemark))
    http.HandleFunc("/1/remark/get/", errorHandler(getRemark))
    http.HandleFunc("/1/remark/delete/", errorHandler(deleteRemark))
}
...
func getRemarkID(path string) (int64, bool) {
    split := strings.Split(path, "/", -1)
    length := len(split)
    for i := 0; i<len(split); i++ {
        if (split[i] == "get" || split[i] == "delete") &&
            (i+1 < length) { // next word may not exist
            id, err := strconv.Atoi64(split[i+1])
            return id, (err == nil)
        }
    }
    return 0,false
}
...
// Handle a delete request of remarks
func deleteRemark(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    switch r.Method {
    case DELETE:
        c.Infof("Deleteing Remark\n")
        c.Infof("Requested URL: %s\n", r.RawURL)
        c.Infof("Requested URL path: %s\n", r.URL.Path)
        // Check if we were requested for a specific remark
        id, foundID := getRemarkID(r.URL.Path)
        if !foundID {
            msg := "Specified remark does not exist\n"
            c.Infof(msg)
            http.Error(w, msg, http.StatusNotFound)
            return
        } else { // foun ID within URL
            query := datastore.NewQuery(REMARK_KIND).Filter("Time=", id)

            // check if any results exist
            count, err := query.Count(c)
            if err != nil {
                http.Error(w, err.String(), http.StatusInternalServerError)
                return
            }
            
            if count == 0 {
                msg := "Specified remark does not exist\n"
                c.Infof(msg)
                http.Error(w, msg, http.StatusNotFound)
                return
            }

            // remove remark with given ID
            itr := query.Run(c)
            for {
                var remark Remark
                key, err := itr.Next(&remark)
                if err == datastore.Done {
                    break
                } else if err != nil {
                    http.Error(w, err.String(), http.StatusInternalServerError)
                    return
                }
                c.Infof("Removing id:%d remark:%s", key.IntID(), remark.Content)
                datastore.Delete(c, key)
            }
        }
    default:
        msg := "Invalid request\n"
        c.Infof(msg)
        http.Error(w, msg, http.StatusBadRequest)
    }
}
...

getと違うのはIDが指定されなかったらエラーを返す点と、クエリーの結果見つかったkeyに対してdatastore.Deleteを呼んで、keyを消してる点くらい。

クライアントサイド

クライアントサイドもやっぱりgetと限りなく似ていて、DELETEは若干マイナーなのか、GETみたいな便利なAPIが無いからhttp.Requestを自分で作ってからhttp.Client.Doを呼ぶ一手間が増えたくらい。

package main

import (
    "http"
    "fmt"
    "os"
)

func main() {
    client := new(http.Client)
    remarkUrlRoot := "http://localhost:8080/1/remark/"

    deleteUrl := remarkUrlRoot + "delete/1312292876000000"
    request, err := http.NewRequest("DELETE", deleteUrl, nil)
    if err != nil {
        fmt.Printf("Error: %s",err)
        return
    }
    
    r, err := client.Do(request)
    if err != nil {
        fmt.Printf("Error: %s",err)
        return
    }
    fmt.Println(r.Status)
    
    bufSize := 256
    buf := make([]byte, bufSize)
    for {
        read, err := r.Body.Read(buf)
        if read == 0 && err == os.EOF {
            fmt.Println("Finished reading")
            break
        } else if err != nil {
            fmt.Printf("Error during read: %s",err)
            break
        }
        // convert the buffer to a string and print it
        fmt.Println(string(buf[0:read]))
    }
    r.Body.Close()
}

サーバーサイドの出力

INFO     2011-08-02 13:55:43,693 __init__.py:347] building _go_app
INFO     2011-08-02 13:55:44,656 __init__.py:333] running _go_app
2011/08/02 13:55:44 INFO: Deleteing Remark
2011/08/02 13:55:44 INFO: Requested URL: /1/remark/delete/1312292876000000
2011/08/02 13:55:44 INFO: Requested URL path: /1/remark/delete/1312292876000000
2011/08/02 13:55:44 INFO: Removing id:1312292876 remark:aardvark
INFO     2011-08-02 13:55:44,771 dev_appserver.py:4248] "DELETE /1/remark/delete/1312292876000000 HTTP/1.1" 200 -

クライアントサイドの出力

$ ./client 
200 OK
Finished reading

次回

次回はPUT。それでこの連載は一旦終わりかなー。

(続く)

2011年7月23日土曜日

Creating an original REST API using Google App Engine and Go (5.1)

2011/8/2追記: コード修正

REST APIを作るための記事を名乗っていながら、前回書いた記事では全然RESTっぽいことやってなかったことに気付いた。ということでAPI部分から変えてしきりなおし!

コメント取得用のAPI

RESTは(GETに関して言えば)極力URLで表現しようよ、というスタンスなので、前回のようにパラメータで欲しいコメントを取得するのは実はあまりREST的でない。なので、APIもよりREST的な形に変更する。

http://localhost:8080/1/remark/get/[コメントのID]

前回と違って、get以降に取得したいコメントのIDをそのままURLで指定するようにした。

サーバーサイド

APIの変更にあわせてサーバーサイドのコードも少し変える。前回からの変更点は以下の通り。

  • ".../get/"以降に直接コメントのURLが指定されてたら指定されたコメントだけを返す。
  • ".../get/"までしか指定されてなかったらコメントを全て返す。

当然、存在しないコメントのIDを指定された場合の適切なエラー処理も必要。ということで以下がそのコード。

func init() {
    http.HandleFunc("/", errorHandler(root))
    http.HandleFunc("/1/remark/post/", errorHandler(postRemark))
    http.HandleFunc("/1/remark/get/", errorHandler(getRemark))
}
...
func getRemarkID(path string) (int64, bool) {
    split := strings.Split(path, "/", -1)
    length := len(split)
    for i := 0; i<len(split); i++ {
        if split[i] == "get" &&
            i+1 < length { // next word may not exist
            id, err := strconv.Atoi64(split[i+1])
            return id, (err == nil)
        }
    }
    return 0,false
}

// Handle a get request of remarks
func getRemark(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    switch r.Method {
    case GET:
        c.Infof("Requested URL: %s\n", r.RawURL)
        c.Infof("Requested URL path: %s\n", r.URL.Path)
        // Check if we were requested for a specific remark
        id, foundID := getRemarkID(r.URL.Path)
        
        var query *datastore.Query
        if  foundID {
            // If we were, create a query to get that remark
            c.Infof("Retrieving specified remark id: %d\n", id)
            query = datastore.NewQuery(REMARK_KIND).Filter("Time=", datastore.Time(id))
        } else {
            // If we weren't, create a query to get all the remarks (new remarks come first)
            c.Infof("Retrieving all remarks\n")
            query = datastore.NewQuery(REMARK_KIND).Order("-Time")
        }
        
        // check if any results exist
        count, err := query.Count(c)
        if err != nil {
            http.Error(w, err.String(), http.StatusInternalServerError)
            return
        }
        
        if count == 0 {
            msg := "Specified remark does not exist\n"
            c.Infof(msg)
            http.Error(w, msg, http.StatusNotFound)
            return
        }
        
        // We have a result.
        // query for it.
        itr := query.Run(c)
        buf := bytes.NewBuffer(nil) // a variable sized buffer
        for {
            var remark Remark
            key, err := itr.Next(&remark)
            if err == datastore.Done {
                break
            } else if err != nil {
                http.Error(w, err.String(), http.StatusInternalServerError)
                return
            }
            c.Infof("Retrieved id:%d remark:%s", key.IntID(), remark.Content)
            fmt.Fprintf(buf, "id=%d&remark=%s\n", remark.Time, remark.Content)
        }

        w.Header().Set("Content-Type", "text/plain; charset=utf-8")
        io.Copy(w, buf)
    default:
        msg := "Invalid request\n"
        c.Infof(msg)
        http.Error(w, msg, http.StatusBadRequest)
    }
}
...

前回よりコードが長くなっている。注意するのは、http.HandleFunc("/1/remark/get/", errorHandler(getRemark))のようにハンドラを指定するとき、URLの末尾にはしっかりスラッシュをつけるということ。こうすることで、getから先にまだURLが続いていても同じハンドラ関数が呼ばれるようになる。このスラッシュをちゃんと指定しないと、"/1/remark/get/123151..."のようなURLへリクエストを出したときはgetRemarkハンドラが呼ばれなくなってしまう。

クライアントサイド

クライアント側はコメントのIDをURLに追加するくらいで、それほど変更点は無い。

package main

import (
    "http"
    "fmt"
    "os"
)

func main() {
    client := new(http.Client)
    remarkUrlRoot := "http://localhost:8080/1/remark/"

    getUrl := remarkUrlRoot + "get/1311387270000000"
    r, err := client.Get(getUrl)
    if err != nil {
        fmt.Printf("Error: %s",err)
        return
    }
    fmt.Println(r.Status)
    
    bufSize := 256
    buf := make([]byte, bufSize)
    for {
        read, err := r.Body.Read(buf)
        if read == 0 && err == os.EOF {
            fmt.Println("Finished reading")
            break
        } else if err != nil {
            fmt.Printf("Error during read: %s",err)
            break
        }
        // convert the buffer to a string and print it
        fmt.Println(string(buf[0:read]))
    }
    r.Body.Close()
}

サーバーサイドの出力

2011/07/23 12:17:09 INFO: Requested URL: /1/remark/get/1311387270000000
2011/07/23 12:17:09 INFO: Requested URL path: /1/remark/get/1311387270000000
2011/07/23 12:17:09 INFO: Retrieving specified remark id: 1311387270000000
2011/07/23 12:17:09 INFO: Retrieved id:1311387270 remark:aardvark
INFO     2011-07-23 12:17:09,169 dev_appserver.py:4248] "GET /1/remark/get/1311387270000000 HTTP/1.1" 200 -

クライアントサイドの出力

200 OK
id=1311387270000000&remark=aardvark

Finished reading

次回

ということで改めてGET用のAPIを作ったところで、次回はDELETEあたりをやることにする。

(続く)

2011年6月27日月曜日

Creating an original REST API using Google App Engine and Go (5)

2011/7/23追記: r58.1に伴うAPIの変更部分を修正

前回まででコメントの格納ができるようになった(っぽい)ので、今度はそれを取得できるようにして実際入ってることを確認する。

コメント取得用のAPI

まずはコメント取得用のAPIを作るところから考える。投稿用のAPIと同じ形を踏襲するので、特に深く考えることなく以下のURLにする。

http://localhost:8080/1/remark/get

あとはここのURLに対してのリクエストをハンドルする処理をサーバーサイドに追加していく。

サーバーサイド

そろそろ長くなってきたので、差分を載せていくことにする。下記がコメントの取得用APIを追加した部分のコード。

...
func init() {
    http.HandleFunc("/", errorHandler(root))
    http.HandleFunc("/1/remark/post", errorHandler(postRemark))
    http.HandleFunc("/1/remark/get", errorHandler(getRemark))
}
...

// Handle a get request of remarks
func getRemark(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    switch r.Method {
    case GET:
        c.Infof("Retrieving Remarks\n")

        // get all the remarks (new remarks come first)
        query := datastore.NewQuery(REMARK_KIND).Order("-Time")
        itr := query.Run(c)

        buf := bytes.NewBuffer(nil) // a variable sized buffer
        for {
            var remark Remark
            key, err := itr.Next(&remark)
            if err == datastore.Done {
                break
            } else if err != nil {
                http.Error(w, err.String(), http.StatusInternalServerError)
                break
            }
            c.Infof("Retrieved id:%d remark:%s", key.IntID(), remark.Content)
            fmt.Fprintf(buf, "id=%d&remark=%s\n", remark.Time, remark.Content)
        }
        w.Header().Set("Content-Type", "text/plain; charset=utf-8")
        io.Copy(w, buf)
    default:
        msg := "Invalid request\n"
        c.Infof(msg)
        http.Error(w, msg, http.StatusBadRequest)
    }
}

処理の流れは以下の通り。重要なのは主にクエリをどう生成するか、という部分だろうか。

  1. コメント取得用のクエリを生成する
  2. クエリを実行して返ってきた結果を走査しつつ、レスポンス用にデータを整形する
  3. 適切なヘッダを付加してレスポンスを送り返す

ちょっとわかりづらいのはOrder("-Time")という部分で、ここは取得したデータのうち、Timeというフィールドの値の大小に応じて並びかえろと指定している。TimeというフィールドはRemark構造体に自分で勝手に定義したフィールドだから、別にここに来るフィールド名自体は何でもいい。あと、デフォルトでは昇順で動くので、頭にマイナス記号をつけて降順にしている。こうすることで、時間的には新しいコメントが先にくるよう並びかえられる、というわけ。

あと今は特に引数をあたえることなく決まった動作(全部のコメントを新しい順に取得する)しかしてないけど、getのAPI的にはもっと細かな制御ができるようにしないといけない。でもそれはひとまずあとまわし。

クライアントサイド

引き続きクライアントサイドのコード。こっちはまだ短いので全部載せる。

package main

import (
    "http"
    "fmt"
    "os"
)

func main() {
    client := new(http.Client)
    remarkUrlRoot := "http://localhost:8080/1/remark/"
    getUrl := remarkUrlRoot + "get"
    r, err := client.Get(getUrl)
    if err != nil {
        fmt.Printf("Error: %s",err)
        return
    }
    fmt.Println(r.Status)
    
    bufSize := 256
    buf := make([]byte, bufSize)
    for {
        read, err := r.Body.Read(buf)
        if read == 0 && err == os.EOF {
            fmt.Println("Finished reading")
            break
        } else if err != nil {
            fmt.Printf("Error during read: %s",err)
            break
        }
        // convert the buffer to a string and print it
        fmt.Println(string(buf[0:read]))
    }
    r.Body.Close()
}

指定されたURLにGetを発行して、返ってきたResponseのBodyを読みつつ表示する、という処理をやっている。決まりきった動作なので特に説明するところは無さそう。Goではこうやるんですよー的な見本かな。

出力結果

サーバーサイドとクライアントサイドの出力結果をそれぞれのせておく。ちゃんとクエリ通りの順番で取得できていることが確認できる。

サーバーサイドの出力結果

INFO     2011-06-26 15:19:20,629 __init__.py:324] building _go_app
INFO     2011-06-26 15:19:21,554 __init__.py:316] running _go_app
2011/06/26 15:19:21 INFO: Retrieving Remarks
2011/06/26 15:19:21 INFO: Retrieved id:1309056182 remark:aardvark
2011/06/26 15:19:21 INFO: Retrieved id:1309056155 remark:aardvark
2011/06/26 15:19:21 INFO: Retrieved id:1308993994 remark:aardvark
2011/06/26 15:19:21 INFO: Retrieved id:1308993976 remark:aardvark
INFO     2011-06-26 15:19:21,671 dev_appserver.py:4217] "GET /1/remark/get HTTP/1.1" 200 -

クライアントサイドの出力結果

200 OK
id=1309056182000000&remark=aardvark
id=1309056155000000&remark=aardvark
id=1308993994000000&remark=aardvark
id=1308993976000000&remark=aardvark

Finished reading

次回

PostしてGetするところまではできたから、残るは(REST的には)DeleteとPut。ということで、次はたぶん投稿したコメントを削除するDeleteのAPIをやるかな。

(続く)

2011年6月26日日曜日

Creating an original REST API using Google App Engine and Go (4)

前回まででコメントを送信する部分はできたので、今回はそのコメントを格納する部分をやる。

Datastore

Google App Engineは自前でDatastoreというデータベースを持っていて、何らかのデータを格納しようと思ったらそこを使うことになる。詳しくは公式のドキュメントを読んでもらうとして、ざっくりと特徴をまとめると以下のようになる。

  • 超スケールする
  • 色んな場所に自動で複製をつくる
  • スキーマという概念が無い。なので、事前に定義したテーブルの型に縛られるようなことなく好きな形のデータを格納できる。
  • SQLでやりとりするとかはできない

DatastoreとのやりとりはQuotaと、ひいては課金と深く関わってくるので注意したほうがいいけど、今回はローカルでしか実行しないつもりなのでひとまずその辺は無視する。

サーバーサイド

以前、APIにとって重要な点はインタフェースが変わらないことである! と偉そうなことを言いながら、早々にAPIを変えることにした。といっても変わったのはURLだけだけど。どうもsendだと他のAPIと統一感が無い点が気になった。ということで、前回のsendからpostに変更して、更にコメントの格納処理を追加したコードが以下。

package hello

import (
    "fmt"
    "http"
    "template"
    "time"
    "appengine"
    "appengine/datastore"
)

const (
    GET string = "GET"
    POST string = "POST"
    DELETE string = "DELETE"
    REMARK_FIELD string = "remark"
    REMARK_KIND string = "Remark"
)

type Remark struct {
    Content string // the remark itself
    Time datastore.Time // the time the remark arrived
}

var (
    errorTemplate  = template.MustParseFile("error.html", nil)
)


func init() {
    http.HandleFunc("/", errorHandler(root))
    http.HandleFunc("/1/remark/post", errorHandler(postRemark))
}

func root(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "At root!")
}

// Handle a remark that was sent
func postRemark(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    
    switch r.Method {
    case POST:
        r.ParseForm()
        remark, present := r.Form[REMARK_FIELD]
        if !present { // required field does not exist
            msg := "Required field does not exist\n"
            c.Infof(msg)
            http.Error(w, msg, http.StatusBadRequest)
        } else { // Got required field
            c.Infof("Remark: %s\n", remark)

            // Store remark to datastore
            
            // Since we only have a single thread now, we specify a constant string as the kind.
            // In the future, we may use a unique string that is tied to the thread.
            kind := REMARK_KIND
            
            currentTime := time.Seconds() // now
            store := Remark {
                Content : remark[0],
                Time : datastore.SecondsToTime(currentTime), 
            }

            // In the future, we may assign a key for each thread,
            // and give that key as parent for each remark in that thread.
            var parentKey *datastore.Key = nil
            var stringID string = "" // using the intID, so we set an empty string for the stringID
            intID := currentTime // using the current time as an int ID
            key := datastore.NewKey(kind, stringID, intID, parentKey)
            _, err := datastore.Put(c, key, &store)
            if err != nil {
                http.Error(w, err.String(), http.StatusInternalServerError)
            }

            c.Infof("Stored remark as id: %d", intID)
        }
    default:
        msg := "Invalid request\n"
        c.Infof(msg)
        http.Error(w, msg, http.StatusBadRequest)
    }
}

// errorHandler wraps the argument handler with an error-catcher that
// returns a 500 HTTP error if the request fails (calls check with err non-nil).
func errorHandler(fn http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                http.Error(w, "Unknown error", http.StatusInternalServerError)
                errorTemplate.Execute(w, err)
            }
        }()
        fn(w, r)
    }
}

上のコードのなかでも、特にキモとなるのは以下の部分。

// Since we only have a single thread now, we specify a constant string as the kind.
            // In the future, we may use a unique string that is tied to the thread.
            kind := REMARK_KIND
            
            currentTime := time.Seconds() // now
            store := Remark {
                Content : remark[0],
                Time : datastore.SecondsToTime(currentTime), 
            }

            // In the future, we may assign a key for each thread,
            // and give that key as parent for each remark in that thread.
            var parentKey *datastore.Key = nil
            var stringID string = "" // using the intID, so we set an empty string for the stringID
            intID := currentTime // using the current time as an int ID
            key := datastore.NewKey(kind, stringID, intID, parentKey)
            _, err := datastore.Put(c, key, &store)

処理の内容は下の感じ。

  1. datastore格納用の構造体にデータをつめる
  2. datastore格納に必要な一意の鍵をつくる
  3. 鍵とデータを一緒に格納する

鍵の生成が少しとまどうところで、調べた感じ基本的には2種類のIDが必要だということがわかった。一つはkindという文字列、あとは数値のIDか文字列のIDのどちらか(もしくは両方)を用意しないといけない。

kindは格納する情報が持つタグのようなもので、後程データを取得するクエリを書くときにもkindを指定することになる。今回は想定しているスレッドが一つだけなので固定の文字列だけど、将来的に複数のスレッドごとにコメントを管理するとなったら(例えば)そのスレッドのタイトルをkindとして指定するとクエリが出しやすいはず。

もう一つの文字列ID、あるいは数値IDは文字通りのIDで、上のサンプルでは現在時刻をそのコメントの数値IDとして設定している。

ちなみに鍵には親鍵を指定することもできて、将来的にスレッドごとにコメントを格納して管理するとなった場合、そのスレッドの鍵を各コメントの親鍵として設定しておくとやっぱりクエリが出しやすいと思う。まだ試してはいないけど。

クライアントサイド

クライアント側は、前回のソースのうちPOSTする先のURLをhttp://local.../sendからhttp://local.../postに変更すればそのまま使える。

サーバーサイド実行結果

さてこの状態で実行をすると、下のような結果が返ってくる。ログを見る限りではちゃんと格納できてるっぽいけど、ちゃんと確認するには実際に取得できるかをみないといけない。ということで次回はその取得する部分を書いていく。

INFO     2011-06-26 02:43:01,856 __init__.py:324] building _go_app
INFO     2011-06-26 02:43:02,649 __init__.py:316] running _go_app
2011/06/26 02:43:02 INFO: Remark: [aardvark]
2011/06/26 02:43:02 INFO: Stored remark as id: 1309056182
INFO     2011-06-26 02:43:02,760 dev_appserver.py:4217] "POST /1/remark/post HTTP/1.1" 200 -
(続く)

2011年6月21日火曜日

Creating an original REST API using Google App Engine and Go (3)

2011/7/23追記: r58.1に伴うAPIの変更部分を修正

だらだらと記事を書いている間にGoogle App Engine SDKの1.5.1がリリースされて、GoでもChannel APIがサポートされるようになったみたい。まぁそれはおいおいやっていくとして、とりあえず話を続ける。

サーバーサイドのサンプル

前回"http://localhost:8080/1/remark/send"に対してPOSTするAPIをとりあえず作ってみよう、ということになったので、早速サーバーサイドからコードを書いてみる。エラーハンドリングの部分はmostachioのサンプルから引っ張ってきてるので詳しくはそちらを参照。

package hello

import (
    "fmt"
    "http"
    "template"
    "appengine"
)

const (
    GET string = "GET"
    POST string = "POST"
    DELETE string = "DELETE"
    REMARK_FIELD = "remark"
)

var (
    errorTemplate  = template.MustParseFile("error.html", nil)
)

func init() {
    http.HandleFunc("/", errorHandler(root))
    http.HandleFunc("/1/remark/send", errorHandler(sendRemark))
}

func root(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "At root!")
}

// Handle a remark that was sent to the service
func sendRemark(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)

    switch r.Method {
    case POST:
        r.ParseForm()
        remark, present := r.Form[REMARK_FIELD]
        if !present { // required field does not exist
            c.Infof("Required field does not exist\n")
            w.WriteHeader(http.StatusBadRequest)
        } else { // Got required field
            c.Infof("Remark: %s\n", remark)

            // process the remark here...
        }
    default:
        w.WriteHeader(http.StatusBadRequest)
        c.Infof("Invalid request\n")
    }
}


// errorHandler wraps the argument handler with an error-catcher that
// returns a 500 HTTP error if the request fails (calls check with err non-nil).
func errorHandler(fn http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                w.WriteHeader(http.StatusInternalServerError)
                errorTemplate.Execute(w, err)
            }
        }()
        fn(w, r)
    }
}

キモはsendRemark関数の中で、処理としては以下のことをやっている。

  1. POSTかどうかを判定する。POSTじゃなかったらエラー。
  2. POSTだったら、"remark"というフィールドがちゃんと送られてるか確認する。フィールドが無ければエラー。
  3. remarkとして送られてきた内容を取得して、処理を行う。いまはまだログとして出力してるだけ。

クライアントサイドのサンプル

サーバーサイドの原型ができたところで、今度はクライアントサイドも作ってみる。こっちは別にGoでなくてもいいんだけど、せっかくなのでGoで書く。

package main

import (
    "http"
    "fmt"
)

func main() {
    client := new(http.Client)
    url := "http://localhost:8080/1/remark/send"
    data := http.Values { 
        "remark": {"aardvark", },
        "blah": {"blah", },
    }
    r, err := client.PostForm(url, data)
    if err != nil {
        fmt.Printf("Error: %s",err)
    }
    fmt.Println(r.Status)
}

内容としては、APIとして用意したURLに対して適切なフィールドと一緒にPOSTする、という極めて単純なコード。

出力結果

ということでサーバーをあげてクライアントを実行したときの両方の出力結果を見てみる。

サーバーサイドの出力

INFO     2011-06-21 14:01:03,593 __init__.py:324] building _go_app
INFO     2011-06-21 14:01:04,390 __init__.py:316] running _go_app
2011/06/21 14:01:04 INFO: Remark: [aardvark]
INFO     2011-06-21 14:01:04,501 dev_appserver.py:4217] "POST /1/remark/send HTTP/1.1" 200 -

クライアントサイドの出力

$ ./client 
200 OK
$

remarkフィールド付きの適切なPOSTを送ったので、期待通りの結果が返ってきている。ここでもしremarkの部分が存在しなかったとしたら以下の感じになる。

サーバーサイドのエラー出力

2011/06/21 14:19:49 INFO: Required field does not exist
INFO     2011-06-21 14:19:49,131 dev_appserver.py:4217] "POST /1/remark/send HTTP/1.1" 400 -

クライアントサイドのエラー出力

$ ./client 
400 Bad Request
$

期待通り、ちゃんとエラーが返ってきている。

次のステップ

そんな感じで、POSTされた発言を取りだすところまではひとまずできた。次は実際に発言を処理するところをやっていきたい。

余談

Google App Engine SDK for Go 1.5.1にしたところ、LoggingのAPIが変わったみたいで早速コンパイルエラーが出てた。

新API

func Logger(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    c.Infof("Requested URL: %v", r.URL)
}

旧API

func Logger(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    c.Logf("Requested URL: %v", r.URL)
}

(続く)

2011年6月12日日曜日

Creating an original REST API using Google App Engine and Go (2)

前回で簡単な動機付けを行ったので、今回はもう少しAPIの詳細に入っていく。

APIの設計

現実に利用されているRESTなAPIというと色々あるけど、ひとまず一番実績のありそうなTwitterを参考にすることにする。というわけでここの資料を読む。なんか色々書いてあるけど、とりあえず現時点で知りたいのは1点だけ。

  • そもそもURLはどういう風になっているのか?

APIにとって重要な点の一つはインタフェースが変わらないこと、変わるとしても以前のAPIがそのまま使用できる互換性を維持することなので、その辺Twitterがどうやってるのかを見たい。

で実際いくつかのAPIのURLを持ってきた。

  • http://api.twitter.com/version/statuses/public_timeline.format
  • http://api.twitter.com/1/users/show.format
  • http://api.twitter.com/1/friendships/create/id.format

見ると、URLは基本的に以下の要素で構成されていることがわかる。

  1. ルートドメイン(http://api.twitter.com/のあたり)
  2. APIのバージョン番号(/1/のあたり)
  3. APIの種類(/statues/とか/users/のあたり)
  4. 実際のコマンド(/public_timelineとか/create/idのあたり)
  5. サーバーに要求しているレスポンスの種類(formatのあたり。実際に入るのはxmlとかrssとかの拡張子)

APIのバージョン番号を直接URLに埋め込んで互換性を維持している点が特徴と言えば特徴で、わりと合理的な感じ。ということでこの構成をそのまま拝借することにする。

想定するサービス

APIを作るにはそもそもどういうサービスを作るかを想定しなきゃならない。今回はサンプルなので、シンプルに以下のようなサービスを想定する。ブログのコメント欄のようなもの、といえばわかりやすいか。

  • 匿名な利用者が発言を書きこめる。
  • 書き込まれた発言は時系列順に誰でも閲覧することができる。

これだけ。

発言を送るためのAPI

サービスの細かな仕様は作りながら考えていくとして、ひとまず発言をサーバーへ送るためのAPIを設計する。URLは先ほどの構成を拝借して、以下のような形にする。ちなみに、見ればわかるけどサーバーはローカルで動いてるものを対象としている。

  • http://localhost:8080/1/remark/send

このURLに対してPOSTすると、発言がサーバーサイドで蓄積される、ということ。

(続く)

2011年6月5日日曜日

Creating an original REST API using Google App Engine and Go

動機

今後のウェブサービスは何らかのAPIを公開するのが必須な気がしている。それは自分のサービスへ外部の開発者を呼び込む意味でもそうだし、そもそもサービスへの窓口をAPIに限定する方が昨今の状況に合っている気がする。結局のところ、ブラウザ上からサービスへアクセスするのは手段の一つに過ぎないわけで、いまはブラウザ以外にもスマートフォン向けのアプリからのアクセスも最初から考慮する必要がある。とすると、ブラウザからの表示を前提としたガチガチに密結合なサービスを作るより、APIだけを設計してブラウザだろうがアプリだろうが共通の窓口から通るようにした方が嬉しいはず。テストとか自動化できるし。クライアントのバグとサーバーのバグを明確に区別できるし。

そんな感じで納得したところで、いざAPIを設計しようとしたらどうすればいいのか良くわからない。なので、とりあえずTwitterのAPIとかを参考にしながら試しに作っていくことにする。

RESTなAPIとStreamingなAPI

TwitterのAPIを見ると、APIが主に2種類あることがわかる。一つはREST APIで、もう一つがStreaming API。これはどう違うかというと、サーバーとクライアント間の通信方法が違う。いや厳密に言うとどちらのAPIもHTTPで通信していることには変わりないんだけど、RESTなAPIがリクエスト->リプライ->別のリクエスト->リプライ…という風に何度も通信をつなぎなおしながらやりとりするのに対し、StreamingなAPIは一旦通信をつないだあとは基本的に通信を切らずにやりとりを行う。

Streamingな方式がサーバー側にとって嬉しい点は2つほどあって、一つはサーバー側からクライアント側へ向けて通知(Push)することができるということ、あともう一つはいちいち接続をつなぎなおす手間が無くなるので、わりと負荷が軽くなること。(余談だけど、Googleが以前発表したSPDYというプロトコルもこの「一度つないだらつなぎなおさないでやりとりする」という理念の下に設計されてて、SPDY使ったほうが全然速いよー、という主張もその辺に関係しているはず)

ということで、基本的にはStreamingなAPIには優位な点があるけど、でもシンプルなREST APIもやっぱり必要だよね、というざっくりとした理解で大丈夫。そもそもTwitter並に頻繁にやりとりが発生するサービスはともかく、そんなにやりとりしないサービスならRESTで十分なはず。

Google App Engine上でのAPI

Google App Engine上の話に置きかえてみる。RESTなAPIは問題無くできるとして、じゃぁStreamingなAPIはどうかというと、これができる。多分。多分というのは、自分で検証したわけでは無いから多分。

具体的には、Channel APIというのを使えばStreamingなAPIを提供できるようになるっぽい。ただ。現状はGo版のApp Engine SDKがChannel APIをまだサポートしてないので、ひとまず考えないことにする。

さて前置きがすんだところで、次回以降では実際にRESTなAPIを作っていく。

(続く)

2011年5月29日日曜日

Trying Google App Engine SDK for Go (2)

パニクったらどうなるの

moustachioのサンプルを見ると、ハンドル関数uploadを登録するときにerrorHandlerという関数(関数を返す関数)でラップした上で登録している。これはもしuploadのなかでpanicが起きたときに適切なエラーハンドルをするための措置みたい。

errorHandlerの用法

func init() {
        http.HandleFunc("/", errorHandler(upload))
        ...
}

...
 
// errorHandler wraps the argument handler with an error-catcher that
// returns a 500 HTTP error if the request fails (calls check with err non-nil).
func errorHandler(fn http.HandlerFunc) http.HandlerFunc {
        return func(w http.ResponseWriter, r *http.Request) {
                defer func() {
                        if err, ok := recover().(os.Error); ok {
                                w.WriteHeader(http.StatusInternalServerError)
                                errorTemplate.Execute(w, err)
                        }
                }()
                fn(w, r)
        }
}

わざわざこういうエラーハンドラが存在する必要が良くわからなかったので、このエラーハンドルを通さず意図的にpanicを起こしたらどうなるかを見てみた。

サンプル

package hello

import (
    "fmt"
    "http"
)

func init() {
    http.HandleFunc("/", root)
}

func root(w http.ResponseWriter, r *http.Request) {
    // test panic here to see what happens
    panic("Testing panic")

    ch := make(chan string, 1)
    go func(){
        ch <- "aaaaa!!"
    }()
    fmt.Fprint(w, "At Root! ", <- ch)
}

実行結果

どうやらデフォルトのエラーハンドラが動いて、ブラウザ上はInternal Server Errorが発生したことが伝わるみたい。ただ、クライアントに対して適切なエラーコードが送られたかどうかは定かでない。

スクリーンショット

dev_appserverのログ

$ dev_appserver.py myapp/
Warning: You are using a Python runtime (2.7) that is more recent than the production runtime environment (2.5). Your application may use features that are not available in the production environment and may not work correctly when deployed to production.
INFO     2011-05-29 02:42:43,751 appengine_rpc.py:159] Server: appengine.google.com
INFO     2011-05-29 02:42:43,754 appcfg.py:440] Checking for updates to the SDK.
INFO     2011-05-29 02:42:44,311 appcfg.py:457] The SDK is up to date.
WARNING  2011-05-29 02:42:44,311 datastore_file_stub.py:657] Could not read datastore data from /tmp/dev_appserver.datastore
INFO     2011-05-29 02:42:44,313 rdbms_sqlite.py:58] Connecting to SQLite database '' with file '/tmp/dev_appserver.rdbms'
INFO     2011-05-29 02:42:44,366 dev_appserver_multiprocess.py:637] Running application xclamm on port 8080: http://localhost:8080
INFO     2011-05-29 02:42:52,748 __init__.py:284] building _go_app
INFO     2011-05-29 02:42:53,503 __init__.py:276] running _go_app
panic: Testing panic

runtime.panic+0xa4 /tmp/appengine/google_appengine/goroot/src/pkg/runtime/proc.c:1060
 runtime.panic(0x811c8e4, 0x9762b550)
hello.root+0x4e hello/panic.go:14
 hello.root(0x976a54a0, 0x9762cf60)
http.HandlerFunc·ServeHTTP+0x35 /tmp/appengine/google_appengine/goroot/src/pkg/http/server.go:533
 http.HandlerFunc·ServeHTTP(0x805b7f0, 0x976a54a0, 0x9762cf60, 0x976bc000, 0x805b7f0, ...)
http.*ServeMux·ServeHTTP+0x194 /tmp/appengine/google_appengine/goroot/src/pkg/http/server.go:728
 http.*ServeMux·ServeHTTP(0x9762b2f0, 0x976a54a0, 0x9762cf60, 0x976bc000, 0x9762cf60, ...)
http.*conn·serve+0x202 /tmp/appengine/google_appengine/goroot/src/pkg/http/server.go:499
 http.*conn·serve(0x9762ce40, 0x0)
runtime.goexit /tmp/appengine/google_appengine/goroot/src/pkg/runtime/proc.c:178
 runtime.goexit()
----- goroutine created by -----
http.*Server·Serve+0x1cf /tmp/appengine/google_appengine/goroot/src/pkg/http/server.go:820
(後略)

まとめ

一応デフォルトのエラーハンドラは存在するみたい。でもやっぱり自分でハンドルしたほうがいいね。

  • Go版のGoogle App Engineには(一応)デフォルトのエラーハンドラがいる
  • クライアントに適切なエラーコードを返してるかは定かでない
  • どっちにしろ見た目がしょぼいから自分でエラーハンドルしたほうがいい

2011年5月17日火曜日

Trying Google App Engine SDK for Go

Google App EngineがGoに対応したとかで、とりあえず試してみることにした。

インストール

何はともあれSDKをダウンロードしてきてパスを通す。

$ cd ~/lib
$ wget http://googleappengine.googlecode.com/files/go_appengine_sdk_linux_386-1.5.0.zip
$ unzip go_appengine_sdk_linux_386-1.5.0.zip
$ export PATH=~/lib/google_appengine/:$PATH

サンプルの作成と実行

サンプルの作成手順は本家ドキュメントに任せるとして、ここでは実際の実行結果を見ていく。

$ dev_appserver.py myapp/
Warning: You are using a Python runtime (2.6) that is more recent than the production runtime environment (2.5). Your application may use features that are not available in the production environment and may not work correctly when deployed to production.
INFO     2011-05-17 14:17:10,225 appengine_rpc.py:159] Server: appengine.google.com
INFO     2011-05-17 14:17:10,229 appcfg.py:440] Checking for updates to the SDK.
INFO     2011-05-17 14:17:10,550 appcfg.py:457] The SDK is up to date.
WARNING  2011-05-17 14:17:10,550 datastore_file_stub.py:657] Could not read datastore data from /tmp/dev_appserver.datastore
INFO     2011-05-17 14:17:10,552 rdbms_sqlite.py:58] Connecting to SQLite database '' with file '/tmp/dev_appserver.rdbms'
INFO     2011-05-17 14:17:10,598 dev_appserver_multiprocess.py:637] Running application helloworld on port 8080: http://localhost:8080

これでGoogle App Engineのサーバーがローカルで起動したので、あとはブラウザからhttp://localhost:8080/にアクセスすると、"Hello, World!"と記載されただけのページが表示される。それに併せて、dev_appserver側では以下のログが出てくる。そのログを見ると、実行時にGoのソースをコンパイルして即実行してるのがわかる。

INFO     2011-05-17 14:17:36,341 __init__.py:284] building _go_app
INFO     2011-05-17 14:17:37,011 __init__.py:276] running _go_app
INFO     2011-05-17 14:17:37,080 dev_appserver.py:4200] "GET / HTTP/1.1" 200 -
INFO     2011-05-17 14:17:37,139 dev_appserver.py:4200] "GET /favicon.ico HTTP/1.1" 200 -
INFO     2011-05-17 14:17:40,245 dev_appserver.py:4200] "GET /favicon.ico HTTP/1.1" 200 -