Skip to content

Instantly share code, notes, and snippets.

@heyLu
Created October 3, 2015 08:01
Show Gist options
  • Save heyLu/e257fcd784bb6fb803a2 to your computer and use it in GitHub Desktop.
Save heyLu/e257fcd784bb6fb803a2 to your computer and use it in GitHub Desktop.
An experiment towards a nice http api in Go
// 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