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を作っていく。

(続く)