Created
October 3, 2015 08:01
-
-
Save heyLu/e257fcd784bb6fb803a2 to your computer and use it in GitHub Desktop.
An experiment towards a nice http api in Go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// An experiment towards a nice http api in Go | |
// | |
// What is a "pretty" api? | |
// | |
// - content negotiation (including errors) | |
// - public vs internal errors | |
// - extensible (i.e. additional formats can be added easily) | |
// | |
// The current implementation has the first two, but not the last. | |
package main | |
import ( | |
"encoding/json" | |
"fmt" | |
"github.com/golang/gddo/httputil" | |
"html/template" | |
"log" | |
"net/http" | |
) | |
// # The Spec | |
// | |
// GET /list -> html | |
// GET /list (application/json) -> json | |
// GET /list (oops) -> "unsupported media type" as text | |
// | |
// GET /error -> "internal server error" as html, actual error logged | |
// GET /error (application/json) -> "internal server error" as json, actual error logged | |
// GET /error (oops) -> "unsupported media type" as text, actual error logged? | |
// | |
// GET /public-error -> error as html | |
// GET /public-error (application/json) -> error as json | |
// GET /public-error (oops) -> "unsupported media type as text | |
type Renderable struct { | |
Status int | |
Metadata map[string]interface{} | |
Data interface{} | |
Template *template.Template | |
} | |
func (r Renderable) MarshalJSON() ([]byte, error) { | |
return json.Marshal(r.Data) | |
} | |
func main() { | |
http.HandleFunc("/list", handleRequest(ListAll)) | |
http.HandleFunc("/error", handleRequest(ErrorExample)) | |
http.HandleFunc("/public-error", handleRequest(PublicError)) | |
log.Println("Running on http://localhost:12345") | |
err := http.ListenAndServe("localhost:12345", nil) | |
if err != nil { | |
log.Fatal(err) | |
} | |
} | |
type Post struct { | |
Title string `json:"title"` | |
Content string `json:"content"` | |
} | |
func ListAll(w http.ResponseWriter, req *http.Request) (interface{}, error) { | |
posts := []Post{ | |
Post{"Hello, World!", "This is my first post!!!"}, | |
} | |
return Renderable{ | |
Metadata: map[string]interface{}{ | |
"Title": "All posts", | |
}, | |
Data: posts, | |
Template: template.Must(template.New("").Parse(listAllTemplate)), | |
}, nil | |
} | |
var listAllTemplate = `<!doctype html> | |
<html> | |
<head> | |
<meta charset="utf-8" /> | |
<title>{{ .Metadata.Title }}</title> | |
</head> | |
<body> | |
<h1>{{ .Metadata.Title }}</h1> | |
{{ range .Data }} | |
<article class="post"> | |
<h2>{{ .Title }}</h2> | |
<p>{{ .Content }}</p> | |
</article> | |
{{ end }} | |
</body> | |
</html> | |
` | |
func ErrorExample(w http.ResponseWriter, req *http.Request) (interface{}, error) { | |
return nil, fmt.Errorf("this is an error...") | |
} | |
func PublicError(w http.ResponseWriter, req *http.Request) (interface{}, error) { | |
return RenderableStatus(http.StatusNotImplemented), nil | |
} | |
func handleRequest(handler func(http.ResponseWriter, *http.Request) (interface{}, error)) func(http.ResponseWriter, *http.Request) { | |
return func(w http.ResponseWriter, req *http.Request) { | |
contentType := httputil.NegotiateContentType(req, []string{"text/html", "application/json"}, "") | |
data, err := handler(w, req) | |
if err != nil { | |
log.Printf("Error: %s: %s\n", req.URL.Path, err) | |
render(w, req, contentType, RenderableStatus(http.StatusInternalServerError)) | |
return | |
} | |
renderable, ok := data.(Renderable) | |
if !ok { | |
panic("not implemented") | |
} | |
render(w, req, contentType, renderable) | |
} | |
} | |
func render(w http.ResponseWriter, req *http.Request, contentType string, renderable Renderable) { | |
if renderable.Status == 0 { | |
renderable.Status = 200 | |
} | |
w.WriteHeader(renderable.Status) | |
switch contentType { | |
case "text/html": | |
if renderable.Template != nil { | |
err := renderable.Template.Execute(w, renderable) | |
if err != nil { | |
// FIXME: we might be in a partial response here, i.e. we | |
// probably didn't return valid html. | |
log.Printf("Error: rendering %s: %s\n", req.URL.Path, err) | |
} | |
} else { | |
fmt.Fprint(w, http.StatusText(renderable.Status)) | |
} | |
case "application/json": | |
var err error | |
pretty := req.URL.Query().Get("pretty") == "true" | |
if pretty { | |
data, err := json.MarshalIndent(renderable.Data, "", " ") | |
if err == nil { | |
w.Write(data) | |
w.Write([]byte{'\n'}) | |
} | |
} else { | |
encoder := json.NewEncoder(w) | |
err = encoder.Encode(renderable.Data) | |
} | |
if err != nil { | |
log.Printf("Error: rendering %s: %s\n", req.URL.Path, err) | |
} | |
default: | |
status := http.StatusUnsupportedMediaType | |
http.Error(w, http.StatusText(status), status) | |
} | |
} | |
func RenderableStatus(status int) Renderable { | |
return Renderable{ | |
Status: status, | |
Data: httpStatus{ | |
Status: status, | |
Message: http.StatusText(status), | |
}, | |
} | |
} | |
type httpStatus struct { | |
Status int `json:"status"` | |
Message string `json:"message"` | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment