package main import ( "net/http" "time" "github.com/emicklei/go-restful" "github.com/emicklei/go-restful-swagger12" "google.golang.org/appengine" "google.golang.org/appengine/datastore" "google.golang.org/appengine/user" ) // This example demonstrates a reasonably complete suite of RESTful operations backed // by DataStore on Google App Engine. // Our simple example struct. type Profile struct { LastModified time.Time `json:"-" xml:"-"` Email string `json:"-" xml:"-"` FirstName string `json:"first_name" xml:"first-name"` NickName string `json:"nick_name" xml:"nick-name"` LastName string `json:"last_name" xml:"last-name"` } type ProfileApi struct { Path string } func gaeUrl() string { if appengine.IsDevAppServer() { return "http://localhost:8080" } else { // Include your URL on App Engine here. // I found no way to get AppID without appengine.Context and this always // based on a http.Request. return "http://federatedservices.appspot.com" } } func init() { u := ProfileApi{Path: "/profiles"} u.register() // Optionally, you can install the Swagger Service which provides a nice Web UI on your REST API // You need to download the Swagger HTML5 assets and change the FilePath location in the config below. // Open .appspot.com/apidocs and enter // Place the Swagger UI files into a folder called static/swagger if you wish to use Swagger // http://.appspot.com/apidocs.json in the api input field. // For testing, you can use http://localhost:8080/apidocs.json config := swagger.Config{ // You control what services are visible WebServices: restful.RegisteredWebServices(), WebServicesUrl: gaeUrl(), ApiPath: "/apidocs.json", // Optionally, specify where the UI is located SwaggerPath: "/apidocs/", // GAE support static content which is configured in your app.yaml. // This example expect the swagger-ui in static/swagger so you should place it there :) SwaggerFilePath: "static/swagger"} swagger.InstallSwaggerService(config) } func (u ProfileApi) register() { ws := new(restful.WebService) ws. Path(u.Path). // You can specify consumes and produces per route as well. Consumes(restful.MIME_JSON, restful.MIME_XML). Produces(restful.MIME_JSON, restful.MIME_XML) ws.Route(ws.POST("").To(u.insert). // Swagger documentation. Doc("insert a new profile"). Param(ws.BodyParameter("Profile", "representation of a profile").DataType("main.Profile")). Reads(Profile{})) ws.Route(ws.GET("/{profile-id}").To(u.read). // Swagger documentation. Doc("read a profile"). Param(ws.PathParameter("profile-id", "identifier for a profile").DataType("string")). Writes(Profile{})) ws.Route(ws.PUT("/{profile-id}").To(u.update). // Swagger documentation. Doc("update an existing profile"). Param(ws.PathParameter("profile-id", "identifier for a profile").DataType("string")). Param(ws.BodyParameter("Profile", "representation of a profile").DataType("main.Profile")). Reads(Profile{})) ws.Route(ws.DELETE("/{profile-id}").To(u.remove). // Swagger documentation. Doc("remove a profile"). Param(ws.PathParameter("profile-id", "identifier for a profile").DataType("string"))) restful.Add(ws) } // POST http://localhost:8080/profiles // {"first_name": "Ivan", "nick_name": "Socks", "last_name": "Hawkes"} // func (u *ProfileApi) insert(r *restful.Request, w *restful.Response) { c := appengine.NewContext(r.Request) // Marshall the entity from the request into a struct. p := new(Profile) err := r.ReadEntity(&p) if err != nil { w.WriteError(http.StatusNotAcceptable, err) return } // Ensure we start with a sensible value for this field. p.LastModified = time.Now() // The profile belongs to this user. p.Email = user.Current(c).String() k, err := datastore.Put(c, datastore.NewIncompleteKey(c, "profiles", nil), p) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Let them know the location of the newly created resource. // TODO: Use a safe Url path append function. w.AddHeader("Location", u.Path+"/"+k.Encode()) // Return the resultant entity. w.WriteHeader(http.StatusCreated) w.WriteEntity(p) } // GET http://localhost:8080/profiles/ahdkZXZ-ZmVkZXJhdGlvbi1zZXJ2aWNlc3IVCxIIcHJvZmlsZXMYgICAgICAgAoM // func (u ProfileApi) read(r *restful.Request, w *restful.Response) { c := appengine.NewContext(r.Request) // Decode the request parameter to determine the key for the entity. k, err := datastore.DecodeKey(r.PathParameter("profile-id")) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } // Retrieve the entity from the datastore. p := Profile{} if err := datastore.Get(c, k, &p); err != nil { if err.Error() == "datastore: no such entity" { http.Error(w, err.Error(), http.StatusNotFound) } else { http.Error(w, err.Error(), http.StatusInternalServerError) } return } // Check we own the profile before allowing them to view it. // Optionally, return a 404 instead to help prevent guessing ids. // TODO: Allow admins access. if p.Email != user.Current(c).String() { http.Error(w, "You do not have access to this resource", http.StatusForbidden) return } w.WriteEntity(p) } // PUT http://localhost:8080/profiles/ahdkZXZ-ZmVkZXJhdGlvbi1zZXJ2aWNlc3IVCxIIcHJvZmlsZXMYgICAgICAgAoM // {"first_name": "Ivan", "nick_name": "Socks", "last_name": "Hawkes"} // func (u *ProfileApi) update(r *restful.Request, w *restful.Response) { c := appengine.NewContext(r.Request) // Decode the request parameter to determine the key for the entity. k, err := datastore.DecodeKey(r.PathParameter("profile-id")) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } // Marshall the entity from the request into a struct. p := new(Profile) err = r.ReadEntity(&p) if err != nil { w.WriteError(http.StatusNotAcceptable, err) return } // Retrieve the old entity from the datastore. old := Profile{} if err := datastore.Get(c, k, &old); err != nil { if err.Error() == "datastore: no such entity" { http.Error(w, err.Error(), http.StatusNotFound) } else { http.Error(w, err.Error(), http.StatusInternalServerError) } return } // Check we own the profile before allowing them to update it. // Optionally, return a 404 instead to help prevent guessing ids. // TODO: Allow admins access. if old.Email != user.Current(c).String() { http.Error(w, "You do not have access to this resource", http.StatusForbidden) return } // Since the whole entity is re-written, we need to assign any invariant fields again // e.g. the owner of the entity. p.Email = user.Current(c).String() // Keep track of the last modification date. p.LastModified = time.Now() // Attempt to overwrite the old entity. _, err = datastore.Put(c, k, p) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Let them know it succeeded. w.WriteHeader(http.StatusNoContent) } // DELETE http://localhost:8080/profiles/ahdkZXZ-ZmVkZXJhdGlvbi1zZXJ2aWNlc3IVCxIIcHJvZmlsZXMYgICAgICAgAoM // func (u *ProfileApi) remove(r *restful.Request, w *restful.Response) { c := appengine.NewContext(r.Request) // Decode the request parameter to determine the key for the entity. k, err := datastore.DecodeKey(r.PathParameter("profile-id")) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } // Retrieve the old entity from the datastore. old := Profile{} if err := datastore.Get(c, k, &old); err != nil { if err.Error() == "datastore: no such entity" { http.Error(w, err.Error(), http.StatusNotFound) } else { http.Error(w, err.Error(), http.StatusInternalServerError) } return } // Check we own the profile before allowing them to delete it. // Optionally, return a 404 instead to help prevent guessing ids. // TODO: Allow admins access. if old.Email != user.Current(c).String() { http.Error(w, "You do not have access to this resource", http.StatusForbidden) return } // Delete the entity. if err := datastore.Delete(c, k); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } // Success notification. w.WriteHeader(http.StatusNoContent) }