From c8fee93dfff506eda4bdd0d9a40662c08c23dbe8 Mon Sep 17 00:00:00 2001
From: Will McCutchen <will@mccutch.org>
Date: Fri, 7 Oct 2016 15:42:40 -0700
Subject: [PATCH] Re-org into httpbin package

---
 handlers.go => httpbin/handlers.go            | 38 ++++----
 handlers_test.go => httpbin/handlers_test.go  | 46 ++++++----
 helpers.go => httpbin/helpers.go              |  4 +-
 httpbin/httpbin.go                            | 89 +++++++++++++++++++
 middleware.go => httpbin/middleware.go        | 19 +---
 .../templates}/forms-post.html                |  0
 .../templates}/httpbin.1.html                 |  0
 {templates => httpbin/templates}/index.html   |  0
 {templates => httpbin/templates}/utf8.html    |  0
 main.go                                       | 32 ++-----
 types.go                                      | 38 --------
 11 files changed, 146 insertions(+), 120 deletions(-)
 rename handlers.go => httpbin/handlers.go (59%)
 rename handlers_test.go => httpbin/handlers_test.go (94%)
 rename helpers.go => httpbin/helpers.go (97%)
 create mode 100644 httpbin/httpbin.go
 rename middleware.go => httpbin/middleware.go (74%)
 rename {templates => httpbin/templates}/forms-post.html (100%)
 rename {templates => httpbin/templates}/httpbin.1.html (100%)
 rename {templates => httpbin/templates}/index.html (100%)
 rename {templates => httpbin/templates}/utf8.html (100%)
 delete mode 100644 types.go

diff --git a/handlers.go b/httpbin/handlers.go
similarity index 59%
rename from handlers.go
rename to httpbin/handlers.go
index 98e4aa4..b4768aa 100644
--- a/handlers.go
+++ b/httpbin/handlers.go
@@ -1,16 +1,15 @@
-package main
+package httpbin
 
 import (
 	"encoding/json"
 	"fmt"
-	"html/template"
 	"net/http"
 	"net/url"
 )
 
-// Index must be wrapped by the withTemplates middleware before it can be used
-func index(w http.ResponseWriter, r *http.Request, t *template.Template) {
-	t = t.Lookup("index.html")
+// Index renders an HTML index page
+func (h *HTTPBin) Index(w http.ResponseWriter, r *http.Request) {
+	t := h.templates.Lookup("index.html")
 	if t == nil {
 		http.Error(w, fmt.Sprintf("error looking up index.html"), http.StatusInternalServerError)
 		return
@@ -18,9 +17,9 @@ func index(w http.ResponseWriter, r *http.Request, t *template.Template) {
 	t.Execute(w, nil)
 }
 
-// FormsPost must be wrapped by withTemplates middleware before it can be used
-func formsPost(w http.ResponseWriter, r *http.Request, t *template.Template) {
-	t = t.Lookup("forms-post.html")
+// FormsPost renders an HTML form that submits a request to the /post endpoint
+func (h *HTTPBin) FormsPost(w http.ResponseWriter, r *http.Request) {
+	t := h.templates.Lookup("forms-post.html")
 	if t == nil {
 		http.Error(w, fmt.Sprintf("error looking up index.html"), http.StatusInternalServerError)
 		return
@@ -28,9 +27,9 @@ func formsPost(w http.ResponseWriter, r *http.Request, t *template.Template) {
 	t.Execute(w, nil)
 }
 
-// utf8 must be wrapped by withTemplates middleware before it can be used
-func utf8(w http.ResponseWriter, r *http.Request, t *template.Template) {
-	t = t.Lookup("utf8.html")
+// UTF8 renders an HTML encoding stress test
+func (h *HTTPBin) UTF8(w http.ResponseWriter, r *http.Request) {
+	t := h.templates.Lookup("utf8.html")
 	if t == nil {
 		http.Error(w, fmt.Sprintf("error looking up index.html"), http.StatusInternalServerError)
 		return
@@ -39,7 +38,8 @@ func utf8(w http.ResponseWriter, r *http.Request, t *template.Template) {
 	t.Execute(w, nil)
 }
 
-func get(w http.ResponseWriter, r *http.Request) {
+// Get handles HTTP GET requests
+func (h *HTTPBin) Get(w http.ResponseWriter, r *http.Request) {
 	args, err := url.ParseQuery(r.URL.RawQuery)
 	if err != nil {
 		http.Error(w, fmt.Sprintf("error parsing query params: %s", err), http.StatusBadRequest)
@@ -55,7 +55,8 @@ func get(w http.ResponseWriter, r *http.Request) {
 	writeJSON(w, body, http.StatusOK)
 }
 
-func requestWithBody(w http.ResponseWriter, r *http.Request) {
+// RequestWithBody handles POST, PUT, and PATCH requests
+func (h *HTTPBin) RequestWithBody(w http.ResponseWriter, r *http.Request) {
 	args, err := url.ParseQuery(r.URL.RawQuery)
 	if err != nil {
 		http.Error(w, fmt.Sprintf("error parsing query params: %s", err), http.StatusBadRequest)
@@ -69,7 +70,7 @@ func requestWithBody(w http.ResponseWriter, r *http.Request) {
 		URL:     getURL(r),
 	}
 
-	err = parseBody(w, r, resp)
+	err = parseBody(w, r, resp, h.options.MaxMemory)
 	if err != nil {
 		http.Error(w, fmt.Sprintf("error parsing request body: %s", err), http.StatusBadRequest)
 	}
@@ -78,21 +79,24 @@ func requestWithBody(w http.ResponseWriter, r *http.Request) {
 	writeJSON(w, body, http.StatusOK)
 }
 
-func ip(w http.ResponseWriter, r *http.Request) {
+// IP echoes the IP address of the incoming request
+func (h *HTTPBin) IP(w http.ResponseWriter, r *http.Request) {
 	body, _ := json.Marshal(&ipResponse{
 		Origin: getOrigin(r),
 	})
 	writeJSON(w, body, http.StatusOK)
 }
 
-func userAgent(w http.ResponseWriter, r *http.Request) {
+// UserAgent echoes the incoming User-Agent header
+func (h *HTTPBin) UserAgent(w http.ResponseWriter, r *http.Request) {
 	body, _ := json.Marshal(&userAgentResponse{
 		UserAgent: r.Header.Get("User-Agent"),
 	})
 	writeJSON(w, body, http.StatusOK)
 }
 
-func headers(w http.ResponseWriter, r *http.Request) {
+// Headers echoes the incoming request headers
+func (h *HTTPBin) Headers(w http.ResponseWriter, r *http.Request) {
 	body, _ := json.Marshal(&headersResponse{
 		Headers: r.Header,
 	})
diff --git a/handlers_test.go b/httpbin/handlers_test.go
similarity index 94%
rename from handlers_test.go
rename to httpbin/handlers_test.go
index 57333dc..7df70e1 100644
--- a/handlers_test.go
+++ b/httpbin/handlers_test.go
@@ -1,4 +1,4 @@
-package main
+package httpbin
 
 import (
 	"bytes"
@@ -11,10 +11,18 @@ import (
 	"testing"
 )
 
+const maxMemory = 1024 * 1024
+
+var app = NewHTTPBin(&Options{
+	MaxMemory: maxMemory,
+})
+
+var handler = app.Handler()
+
 func TestIndex(t *testing.T) {
 	r, _ := http.NewRequest("GET", "/", nil)
 	w := httptest.NewRecorder()
-	app().ServeHTTP(w, r)
+	handler.ServeHTTP(w, r)
 
 	if !strings.Contains(w.Body.String(), "go-httpbin") {
 		t.Fatalf("expected go-httpbin in index body")
@@ -24,7 +32,7 @@ func TestIndex(t *testing.T) {
 func TestFormsPost(t *testing.T) {
 	r, _ := http.NewRequest("GET", "/forms/post", nil)
 	w := httptest.NewRecorder()
-	app().ServeHTTP(w, r)
+	handler.ServeHTTP(w, r)
 
 	if !strings.Contains(w.Body.String(), `<form method="post" action="/post">`) {
 		t.Fatalf("expected <form> in body")
@@ -34,7 +42,7 @@ func TestFormsPost(t *testing.T) {
 func TestUTF8(t *testing.T) {
 	r, _ := http.NewRequest("GET", "/encoding/utf8", nil)
 	w := httptest.NewRecorder()
-	app().ServeHTTP(w, r)
+	handler.ServeHTTP(w, r)
 
 	if w.Header().Get("Content-Type") != "text/html; charset=utf-8" {
 		t.Fatalf("expected 'text/html; charset=utf-8' content type")
@@ -49,7 +57,7 @@ func TestGet__Basic(t *testing.T) {
 	r.Host = "localhost"
 	r.Header.Set("User-Agent", "test")
 	w := httptest.NewRecorder()
-	app().ServeHTTP(w, r)
+	handler.ServeHTTP(w, r)
 
 	if w.Code != 200 {
 		t.Fatalf("expected status code 200, got %d", w.Code)
@@ -88,7 +96,7 @@ func TestGet__Basic(t *testing.T) {
 func TestGet__OnlyAllowsGets(t *testing.T) {
 	r, _ := http.NewRequest("POST", "/get", nil)
 	w := httptest.NewRecorder()
-	app().ServeHTTP(w, r)
+	handler.ServeHTTP(w, r)
 
 	if w.Code != http.StatusMethodNotAllowed {
 		t.Fatalf("expected HTTP 405, got %d", w.Code)
@@ -98,7 +106,7 @@ func TestGet__OnlyAllowsGets(t *testing.T) {
 func TestGet__CORSHeadersWithoutRequestOrigin(t *testing.T) {
 	r, _ := http.NewRequest("GET", "/get", nil)
 	w := httptest.NewRecorder()
-	app().ServeHTTP(w, r)
+	handler.ServeHTTP(w, r)
 
 	if w.Header().Get("Access-Control-Allow-Origin") != "*" {
 		t.Fatalf("expected Access-Control-Allow-Origin=*, got %#v", w.Header().Get("Access-Control-Allow-Origin"))
@@ -109,7 +117,7 @@ func TestGet__CORSHeadersWithRequestOrigin(t *testing.T) {
 	r, _ := http.NewRequest("GET", "/get", nil)
 	r.Header.Set("Origin", "origin")
 	w := httptest.NewRecorder()
-	app().ServeHTTP(w, r)
+	handler.ServeHTTP(w, r)
 
 	if w.Header().Get("Access-Control-Allow-Origin") != "origin" {
 		t.Fatalf("expected Access-Control-Allow-Origin=origin, got %#v", w.Header().Get("Access-Control-Allow-Origin"))
@@ -119,7 +127,7 @@ func TestGet__CORSHeadersWithRequestOrigin(t *testing.T) {
 func TestGet__CORSHeadersWithOptionsVerb(t *testing.T) {
 	r, _ := http.NewRequest("OPTIONS", "/get", nil)
 	w := httptest.NewRecorder()
-	app().ServeHTTP(w, r)
+	handler.ServeHTTP(w, r)
 
 	var headerTests = []struct {
 		key      string
@@ -142,7 +150,7 @@ func TestGet__CORSAllowHeaders(t *testing.T) {
 	r, _ := http.NewRequest("OPTIONS", "/get", nil)
 	r.Header.Set("Access-Control-Request-Headers", "X-Test-Header")
 	w := httptest.NewRecorder()
-	app().ServeHTTP(w, r)
+	handler.ServeHTTP(w, r)
 
 	var headerTests = []struct {
 		key      string
@@ -171,7 +179,7 @@ func TestGet__XForwardedProto(t *testing.T) {
 		r, _ := http.NewRequest("GET", "/get", nil)
 		r.Header.Set(test.key, test.value)
 		w := httptest.NewRecorder()
-		app().ServeHTTP(w, r)
+		handler.ServeHTTP(w, r)
 
 		var resp *bodyResponse
 		err := json.Unmarshal(w.Body.Bytes(), &resp)
@@ -189,7 +197,7 @@ func TestIP(t *testing.T) {
 	r, _ := http.NewRequest("GET", "/ip", nil)
 	r.RemoteAddr = "192.168.0.100"
 	w := httptest.NewRecorder()
-	app().ServeHTTP(w, r)
+	handler.ServeHTTP(w, r)
 
 	var resp *ipResponse
 	err := json.Unmarshal(w.Body.Bytes(), &resp)
@@ -206,7 +214,7 @@ func TestUserAgent(t *testing.T) {
 	r, _ := http.NewRequest("GET", "/user-agent", nil)
 	r.Header.Set("User-Agent", "test")
 	w := httptest.NewRecorder()
-	app().ServeHTTP(w, r)
+	handler.ServeHTTP(w, r)
 
 	var resp *userAgentResponse
 	err := json.Unmarshal(w.Body.Bytes(), &resp)
@@ -226,7 +234,7 @@ func TestHeaders(t *testing.T) {
 	r.Header.Add("Bar-Header", "bar1")
 	r.Header.Add("Bar-Header", "bar2")
 	w := httptest.NewRecorder()
-	app().ServeHTTP(w, r)
+	handler.ServeHTTP(w, r)
 
 	var resp *headersResponse
 	err := json.Unmarshal(w.Body.Bytes(), &resp)
@@ -248,7 +256,7 @@ func TestHeaders(t *testing.T) {
 func TestPost__EmptyBody(t *testing.T) {
 	r, _ := http.NewRequest("POST", "/post", nil)
 	w := httptest.NewRecorder()
-	app().ServeHTTP(w, r)
+	handler.ServeHTTP(w, r)
 
 	var resp *bodyResponse
 	err := json.Unmarshal(w.Body.Bytes(), &resp)
@@ -273,7 +281,7 @@ func TestPost__FormEncodedBody(t *testing.T) {
 	r, _ := http.NewRequest("POST", "/post", strings.NewReader(params.Encode()))
 	r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 	w := httptest.NewRecorder()
-	app().ServeHTTP(w, r)
+	handler.ServeHTTP(w, r)
 
 	var resp *bodyResponse
 	err := json.Unmarshal(w.Body.Bytes(), &resp)
@@ -306,7 +314,7 @@ func TestPost__FormEncodedBodyNoContentType(t *testing.T) {
 
 	r, _ := http.NewRequest("POST", "/post", strings.NewReader(params.Encode()))
 	w := httptest.NewRecorder()
-	app().ServeHTTP(w, r)
+	handler.ServeHTTP(w, r)
 
 	var resp *bodyResponse
 	err := json.Unmarshal(w.Body.Bytes(), &resp)
@@ -343,7 +351,7 @@ func TestPost__JSON(t *testing.T) {
 	r, _ := http.NewRequest("POST", "/post", bytes.NewReader(inputBody))
 	r.Header.Set("Content-Type", "application/json; charset=utf-8")
 	w := httptest.NewRecorder()
-	app().ServeHTTP(w, r)
+	handler.ServeHTTP(w, r)
 
 	var resp *bodyResponse
 	err := json.Unmarshal(w.Body.Bytes(), &resp)
@@ -380,7 +388,7 @@ func TestPost__BodyTooBig(t *testing.T) {
 
 	r, _ := http.NewRequest("POST", "/post", bytes.NewReader(body))
 	w := httptest.NewRecorder()
-	app().ServeHTTP(w, r)
+	handler.ServeHTTP(w, r)
 
 	if w.Code != http.StatusBadRequest {
 		t.Fatalf("expected code %d, got %d", http.StatusBadRequest, w.Code)
diff --git a/helpers.go b/httpbin/helpers.go
similarity index 97%
rename from helpers.go
rename to httpbin/helpers.go
index b8daae9..9cac73d 100644
--- a/helpers.go
+++ b/httpbin/helpers.go
@@ -1,4 +1,4 @@
-package main
+package httpbin
 
 import (
 	"encoding/json"
@@ -56,7 +56,7 @@ func writeJSON(w http.ResponseWriter, body []byte, status int) {
 // parseBody handles parsing a request body into our standard API response,
 // taking care to only consume the request body once based on the Content-Type
 // of the request. The given Resp will be updated.
-func parseBody(w http.ResponseWriter, r *http.Request, resp *bodyResponse) error {
+func parseBody(w http.ResponseWriter, r *http.Request, resp *bodyResponse, maxMemory int64) error {
 	if r.Body == nil {
 		return nil
 	}
diff --git a/httpbin/httpbin.go b/httpbin/httpbin.go
new file mode 100644
index 0000000..0302a5b
--- /dev/null
+++ b/httpbin/httpbin.go
@@ -0,0 +1,89 @@
+package httpbin
+
+import (
+	"html/template"
+	"log"
+	"net/http"
+	"net/url"
+)
+
+type headersResponse struct {
+	Headers http.Header `json:"headers"`
+}
+
+type ipResponse struct {
+	Origin string `json:"origin"`
+}
+
+type userAgentResponse struct {
+	UserAgent string `json:"user-agent"`
+}
+
+type getResponse struct {
+	Args    url.Values  `json:"args"`
+	Headers http.Header `json:"headers"`
+	Origin  string      `json:"origin"`
+	URL     string      `json:"url"`
+}
+
+// A generic response for any incoming request that might contain a body
+type bodyResponse struct {
+	Args    url.Values  `json:"args"`
+	Headers http.Header `json:"headers"`
+	Origin  string      `json:"origin"`
+	URL     string      `json:"url"`
+
+	Data  []byte              `json:"data"`
+	Files map[string][]string `json:"files"`
+	Form  map[string][]string `json:"form"`
+	JSON  interface{}         `json:"json"`
+}
+
+// Options are used to configure HTTPBin
+type Options struct {
+	MaxMemory int64
+}
+
+// HTTPBin contains the business logic
+type HTTPBin struct {
+	options   *Options
+	templates *template.Template
+}
+
+// Handler returns an http.Handler that exposes all HTTPBin endpoints
+func (h *HTTPBin) Handler() http.Handler {
+	mux := http.NewServeMux()
+
+	mux.HandleFunc("/", methods(h.Index, "GET"))
+	mux.HandleFunc("/forms/post", methods(h.FormsPost, "GET"))
+	mux.HandleFunc("/encoding/utf8", methods(h.UTF8, "GET"))
+
+	mux.HandleFunc("/get", methods(h.Get, "GET"))
+	mux.HandleFunc("/post", methods(h.RequestWithBody, "POST"))
+	mux.HandleFunc("/put", methods(h.RequestWithBody, "PUT"))
+	mux.HandleFunc("/patch", methods(h.RequestWithBody, "PATCH"))
+	mux.HandleFunc("/delete", methods(h.RequestWithBody, "DELETE"))
+
+	mux.HandleFunc("/ip", h.IP)
+	mux.HandleFunc("/user-agent", h.UserAgent)
+	mux.HandleFunc("/headers", h.Headers)
+
+	return logger(cors(mux))
+}
+
+// NewHTTPBin creates a new HTTPBin
+func NewHTTPBin(options *Options) *HTTPBin {
+	if options == nil {
+		options = &Options{}
+	}
+
+	t, err := template.ParseGlob("templates/*.html")
+	if err != nil {
+		log.Fatalf("error parsing templates: %s", err)
+	}
+
+	return &HTTPBin{
+		options:   options,
+		templates: t,
+	}
+}
diff --git a/middleware.go b/httpbin/middleware.go
similarity index 74%
rename from middleware.go
rename to httpbin/middleware.go
index e857f19..8769302 100644
--- a/middleware.go
+++ b/httpbin/middleware.go
@@ -1,8 +1,7 @@
-package main
+package httpbin
 
 import (
 	"fmt"
-	"html/template"
 	"log"
 	"net/http"
 )
@@ -51,19 +50,3 @@ func methods(h http.HandlerFunc, methods ...string) http.HandlerFunc {
 		h.ServeHTTP(w, r)
 	}
 }
-
-// Simplify function signatures below
-type templateHandlerFunc func(http.ResponseWriter, *http.Request, *template.Template)
-
-func withTemplates(path string) func(templateHandlerFunc) http.HandlerFunc {
-	t, err := template.ParseGlob(path)
-	if err != nil {
-		log.Fatalf("error parsing templates: %s", err)
-	}
-
-	return func(handler templateHandlerFunc) http.HandlerFunc {
-		return func(w http.ResponseWriter, r *http.Request) {
-			handler(w, r, t)
-		}
-	}
-}
diff --git a/templates/forms-post.html b/httpbin/templates/forms-post.html
similarity index 100%
rename from templates/forms-post.html
rename to httpbin/templates/forms-post.html
diff --git a/templates/httpbin.1.html b/httpbin/templates/httpbin.1.html
similarity index 100%
rename from templates/httpbin.1.html
rename to httpbin/templates/httpbin.1.html
diff --git a/templates/index.html b/httpbin/templates/index.html
similarity index 100%
rename from templates/index.html
rename to httpbin/templates/index.html
diff --git a/templates/utf8.html b/httpbin/templates/utf8.html
similarity index 100%
rename from templates/utf8.html
rename to httpbin/templates/utf8.html
diff --git a/main.go b/main.go
index 1f421f6..e4f8310 100644
--- a/main.go
+++ b/main.go
@@ -3,35 +3,15 @@ package main
 import (
 	"log"
 	"net/http"
-)
-
-// Max size of a request body we'll handle
-const maxMemory = 1024*1024*5 + 1
-
-func app() http.Handler {
-	h := http.NewServeMux()
-	templateWrapper := withTemplates("templates/*.html")
-
-	h.HandleFunc("/", methods(templateWrapper(index), "GET"))
-	h.HandleFunc("/forms/post", methods(templateWrapper(formsPost), "GET"))
-	h.HandleFunc("/encoding/utf8", methods(templateWrapper(utf8), "GET"))
 
-	h.HandleFunc("/get", methods(get, "GET"))
-	h.HandleFunc("/post", methods(requestWithBody, "POST"))
-	h.HandleFunc("/put", methods(requestWithBody, "PUT"))
-	h.HandleFunc("/patch", methods(requestWithBody, "PATCH"))
-	h.HandleFunc("/delete", methods(requestWithBody, "DELETE"))
-
-	h.HandleFunc("/ip", ip)
-	h.HandleFunc("/user-agent", userAgent)
-	h.HandleFunc("/headers", headers)
-
-	return logger(cors(h))
-}
+	"github.com/mccutchen/go-httpbin/httpbin"
+)
 
 func main() {
-	a := app()
+	h := httpbin.NewHTTPBin(&httpbin.Options{
+		MaxMemory: 1024 * 1024 * 5,
+	})
 	log.Printf("listening on 9999")
-	err := http.ListenAndServe(":9999", a)
+	err := http.ListenAndServe(":9999", h.Handler())
 	log.Fatal(err)
 }
diff --git a/types.go b/types.go
deleted file mode 100644
index 75a314f..0000000
--- a/types.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package main
-
-import (
-	"net/http"
-	"net/url"
-)
-
-type headersResponse struct {
-	Headers http.Header `json:"headers"`
-}
-
-type ipResponse struct {
-	Origin string `json:"origin"`
-}
-
-type userAgentResponse struct {
-	UserAgent string `json:"user-agent"`
-}
-
-type getResponse struct {
-	Args    url.Values  `json:"args"`
-	Headers http.Header `json:"headers"`
-	Origin  string      `json:"origin"`
-	URL     string      `json:"url"`
-}
-
-// A generic response for any incoming request that might contain a body
-type bodyResponse struct {
-	Args    url.Values  `json:"args"`
-	Headers http.Header `json:"headers"`
-	Origin  string      `json:"origin"`
-	URL     string      `json:"url"`
-
-	Data  []byte              `json:"data"`
-	Files map[string][]string `json:"files"`
-	Form  map[string][]string `json:"form"`
-	JSON  interface{}         `json:"json"`
-}
-- 
GitLab