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月6日土曜日

Tuning Ubuntu's Unity Desktop

普段Google App Engine関連の開発にはUbuntu 11.04を使ってて、評判はともかくとしてUnityはわりと気にいっている。まぁGnome 3もいつかは試したいけど。

Alt+Tabの切り替えが遅い

ただ前から気になってる点があって、なにかというとAlt+Tabでウィンドウを切り替えたときの微妙なもたつき。正直切り替えはもっとサクサク動いて欲しい。しばらく我慢して使ってたけど耐えられなくなってきたので、設定で改善できないか探したらわりとあっさり見つかった。

Compiz Config Settings Manager

まず必要なのはCompiz Config Settings Manager。これたしか標準では入ってないんだけど、Unityまわりのチューニングやらキーバインドを変えようと思ったら必要。自分はこれでウィンドウのタイル配置キーバインド(Ctrl+Alt+5とか)も変えている。テンキーとか無いからー。

Compiz Config Settings Managerをインストールするには以下。

sudo apt-get install compizconfig-settings-manager

表示速度の改善

Compiz Config Settings Managerがインストールされたら、ccsmコマンドで起動して、以下の手順で速度を調整する。

  1. ウィンドウ・マネジメントの項目にあるStatic Application Swicherをクリックする。
  2. Behaviourタブに移動する。
  3. Popup Window Delayを0にする。

以上。これでAlt+Tabを押したらウィンドウ候補が即表示されるようになる。ちなみに、これは値を変えた時点で即反映されるはず。

ワークスペース切り替えもついでに

自分はついでにワークスペース間の切り替えも高速化してて、その方法は以下。

  1. デスクトップの項目にあるDesktop Wallをクリックする。
  2. Viewport Switchingのタブに移動する。
  3. Wall Sliding Durationを0にする。

これでワークスペースをCtrl+Alt+右矢印とかで切りかえたときのスライドアニメーションが無くなって、高速化される。

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。それでこの連載は一旦終わりかなー。

(続く)