1package main 2 3import ( 4 "net/http" 5 "time" 6 7 "github.com/emicklei/go-restful" 8 "github.com/emicklei/go-restful-swagger12" 9 "google.golang.org/appengine" 10 "google.golang.org/appengine/datastore" 11 "google.golang.org/appengine/user" 12) 13 14// This example demonstrates a reasonably complete suite of RESTful operations backed 15// by DataStore on Google App Engine. 16 17// Our simple example struct. 18type Profile struct { 19 LastModified time.Time `json:"-" xml:"-"` 20 Email string `json:"-" xml:"-"` 21 FirstName string `json:"first_name" xml:"first-name"` 22 NickName string `json:"nick_name" xml:"nick-name"` 23 LastName string `json:"last_name" xml:"last-name"` 24} 25 26type ProfileApi struct { 27 Path string 28} 29 30func gaeUrl() string { 31 if appengine.IsDevAppServer() { 32 return "http://localhost:8080" 33 } else { 34 // Include your URL on App Engine here. 35 // I found no way to get AppID without appengine.Context and this always 36 // based on a http.Request. 37 return "http://federatedservices.appspot.com" 38 } 39} 40 41func init() { 42 u := ProfileApi{Path: "/profiles"} 43 u.register() 44 45 // Optionally, you can install the Swagger Service which provides a nice Web UI on your REST API 46 // You need to download the Swagger HTML5 assets and change the FilePath location in the config below. 47 // Open <your_app_id>.appspot.com/apidocs and enter 48 // Place the Swagger UI files into a folder called static/swagger if you wish to use Swagger 49 // http://<your_app_id>.appspot.com/apidocs.json in the api input field. 50 // For testing, you can use http://localhost:8080/apidocs.json 51 config := swagger.Config{ 52 // You control what services are visible 53 WebServices: restful.RegisteredWebServices(), 54 WebServicesUrl: gaeUrl(), 55 ApiPath: "/apidocs.json", 56 57 // Optionally, specify where the UI is located 58 SwaggerPath: "/apidocs/", 59 60 // GAE support static content which is configured in your app.yaml. 61 // This example expect the swagger-ui in static/swagger so you should place it there :) 62 SwaggerFilePath: "static/swagger"} 63 swagger.InstallSwaggerService(config) 64} 65 66func (u ProfileApi) register() { 67 ws := new(restful.WebService) 68 69 ws. 70 Path(u.Path). 71 // You can specify consumes and produces per route as well. 72 Consumes(restful.MIME_JSON, restful.MIME_XML). 73 Produces(restful.MIME_JSON, restful.MIME_XML) 74 75 ws.Route(ws.POST("").To(u.insert). 76 // Swagger documentation. 77 Doc("insert a new profile"). 78 Param(ws.BodyParameter("Profile", "representation of a profile").DataType("main.Profile")). 79 Reads(Profile{})) 80 81 ws.Route(ws.GET("/{profile-id}").To(u.read). 82 // Swagger documentation. 83 Doc("read a profile"). 84 Param(ws.PathParameter("profile-id", "identifier for a profile").DataType("string")). 85 Writes(Profile{})) 86 87 ws.Route(ws.PUT("/{profile-id}").To(u.update). 88 // Swagger documentation. 89 Doc("update an existing profile"). 90 Param(ws.PathParameter("profile-id", "identifier for a profile").DataType("string")). 91 Param(ws.BodyParameter("Profile", "representation of a profile").DataType("main.Profile")). 92 Reads(Profile{})) 93 94 ws.Route(ws.DELETE("/{profile-id}").To(u.remove). 95 // Swagger documentation. 96 Doc("remove a profile"). 97 Param(ws.PathParameter("profile-id", "identifier for a profile").DataType("string"))) 98 99 restful.Add(ws) 100} 101 102// POST http://localhost:8080/profiles 103// {"first_name": "Ivan", "nick_name": "Socks", "last_name": "Hawkes"} 104// 105func (u *ProfileApi) insert(r *restful.Request, w *restful.Response) { 106 c := appengine.NewContext(r.Request) 107 108 // Marshall the entity from the request into a struct. 109 p := new(Profile) 110 err := r.ReadEntity(&p) 111 if err != nil { 112 w.WriteError(http.StatusNotAcceptable, err) 113 return 114 } 115 116 // Ensure we start with a sensible value for this field. 117 p.LastModified = time.Now() 118 119 // The profile belongs to this user. 120 p.Email = user.Current(c).String() 121 122 k, err := datastore.Put(c, datastore.NewIncompleteKey(c, "profiles", nil), p) 123 if err != nil { 124 http.Error(w, err.Error(), http.StatusInternalServerError) 125 return 126 } 127 128 // Let them know the location of the newly created resource. 129 // TODO: Use a safe Url path append function. 130 w.AddHeader("Location", u.Path+"/"+k.Encode()) 131 132 // Return the resultant entity. 133 w.WriteHeader(http.StatusCreated) 134 w.WriteEntity(p) 135} 136 137// GET http://localhost:8080/profiles/ahdkZXZ-ZmVkZXJhdGlvbi1zZXJ2aWNlc3IVCxIIcHJvZmlsZXMYgICAgICAgAoM 138// 139func (u ProfileApi) read(r *restful.Request, w *restful.Response) { 140 c := appengine.NewContext(r.Request) 141 142 // Decode the request parameter to determine the key for the entity. 143 k, err := datastore.DecodeKey(r.PathParameter("profile-id")) 144 if err != nil { 145 http.Error(w, err.Error(), http.StatusBadRequest) 146 return 147 } 148 149 // Retrieve the entity from the datastore. 150 p := Profile{} 151 if err := datastore.Get(c, k, &p); err != nil { 152 if err.Error() == "datastore: no such entity" { 153 http.Error(w, err.Error(), http.StatusNotFound) 154 } else { 155 http.Error(w, err.Error(), http.StatusInternalServerError) 156 } 157 return 158 } 159 160 // Check we own the profile before allowing them to view it. 161 // Optionally, return a 404 instead to help prevent guessing ids. 162 // TODO: Allow admins access. 163 if p.Email != user.Current(c).String() { 164 http.Error(w, "You do not have access to this resource", http.StatusForbidden) 165 return 166 } 167 168 w.WriteEntity(p) 169} 170 171// PUT http://localhost:8080/profiles/ahdkZXZ-ZmVkZXJhdGlvbi1zZXJ2aWNlc3IVCxIIcHJvZmlsZXMYgICAgICAgAoM 172// {"first_name": "Ivan", "nick_name": "Socks", "last_name": "Hawkes"} 173// 174func (u *ProfileApi) update(r *restful.Request, w *restful.Response) { 175 c := appengine.NewContext(r.Request) 176 177 // Decode the request parameter to determine the key for the entity. 178 k, err := datastore.DecodeKey(r.PathParameter("profile-id")) 179 if err != nil { 180 http.Error(w, err.Error(), http.StatusBadRequest) 181 return 182 } 183 184 // Marshall the entity from the request into a struct. 185 p := new(Profile) 186 err = r.ReadEntity(&p) 187 if err != nil { 188 w.WriteError(http.StatusNotAcceptable, err) 189 return 190 } 191 192 // Retrieve the old entity from the datastore. 193 old := Profile{} 194 if err := datastore.Get(c, k, &old); err != nil { 195 if err.Error() == "datastore: no such entity" { 196 http.Error(w, err.Error(), http.StatusNotFound) 197 } else { 198 http.Error(w, err.Error(), http.StatusInternalServerError) 199 } 200 return 201 } 202 203 // Check we own the profile before allowing them to update it. 204 // Optionally, return a 404 instead to help prevent guessing ids. 205 // TODO: Allow admins access. 206 if old.Email != user.Current(c).String() { 207 http.Error(w, "You do not have access to this resource", http.StatusForbidden) 208 return 209 } 210 211 // Since the whole entity is re-written, we need to assign any invariant fields again 212 // e.g. the owner of the entity. 213 p.Email = user.Current(c).String() 214 215 // Keep track of the last modification date. 216 p.LastModified = time.Now() 217 218 // Attempt to overwrite the old entity. 219 _, err = datastore.Put(c, k, p) 220 if err != nil { 221 http.Error(w, err.Error(), http.StatusInternalServerError) 222 return 223 } 224 225 // Let them know it succeeded. 226 w.WriteHeader(http.StatusNoContent) 227} 228 229// DELETE http://localhost:8080/profiles/ahdkZXZ-ZmVkZXJhdGlvbi1zZXJ2aWNlc3IVCxIIcHJvZmlsZXMYgICAgICAgAoM 230// 231func (u *ProfileApi) remove(r *restful.Request, w *restful.Response) { 232 c := appengine.NewContext(r.Request) 233 234 // Decode the request parameter to determine the key for the entity. 235 k, err := datastore.DecodeKey(r.PathParameter("profile-id")) 236 if err != nil { 237 http.Error(w, err.Error(), http.StatusBadRequest) 238 return 239 } 240 241 // Retrieve the old entity from the datastore. 242 old := Profile{} 243 if err := datastore.Get(c, k, &old); err != nil { 244 if err.Error() == "datastore: no such entity" { 245 http.Error(w, err.Error(), http.StatusNotFound) 246 } else { 247 http.Error(w, err.Error(), http.StatusInternalServerError) 248 } 249 return 250 } 251 252 // Check we own the profile before allowing them to delete it. 253 // Optionally, return a 404 instead to help prevent guessing ids. 254 // TODO: Allow admins access. 255 if old.Email != user.Current(c).String() { 256 http.Error(w, "You do not have access to this resource", http.StatusForbidden) 257 return 258 } 259 260 // Delete the entity. 261 if err := datastore.Delete(c, k); err != nil { 262 http.Error(w, err.Error(), http.StatusInternalServerError) 263 } 264 265 // Success notification. 266 w.WriteHeader(http.StatusNoContent) 267} 268