前回で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版も色々と進化してきたのでまたおいおい別の記事も書いていきたい。