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あたりをやることにする。

(続く)