From 282682d6445c93a42bd5aab70e25a3b778737d5f Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Thu, 18 Oct 2018 20:35:35 +0300 Subject: [PATCH] new: request processor (see JSON and XML and /_examples/8_bind_req_send_res)\n Also: allow more than one methods to be passed on muxie.Methods().Handle/HandleFunc to register the same handler for specific methods --- README.md | 7 +- _examples/7_by_methods/main.go | 17 +++ _examples/8_bind_req_send_res/main.go | 77 ++++++++++++++ doc.go | 2 +- method_handler.go | 30 ++++-- method_handler_test.go | 1 - mux_test.go | 22 ++++ request_processor.go | 148 ++++++++++++++++++++++++++ request_processor_test.go | 63 +++++++++++ 9 files changed, 358 insertions(+), 9 deletions(-) create mode 100644 _examples/8_bind_req_send_res/main.go create mode 100644 request_processor.go create mode 100644 request_processor_test.go diff --git a/README.md b/README.md index d957672..0a1cfd8 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,14 @@
- Release/stability + + + Godocs + software engineer +request header: + "Content-Type": "text/xml" +response: + Go value of the request body: + main.person{XMLName:xml.Name{Space:"", Local:"person"}, Name:"kataras", Age:25, Description:"software engineer"} + +Send a response + +request: + GET: http://localhost:8080/get +response header: + "Content-Type": "text/xml; charset=utf-8" (can be modified by muxie.Charset variable) +response: + + software engineer + `) + http.ListenAndServe(":8080", mux) +} diff --git a/doc.go b/doc.go index 02c5472..4dfd085 100644 --- a/doc.go +++ b/doc.go @@ -5,7 +5,7 @@ Source code and other details for the project are available at GitHub: Current Version -1.0.0 +1.0.3 Installation diff --git a/method_handler.go b/method_handler.go index f6af82e..2ad7b46 100644 --- a/method_handler.go +++ b/method_handler.go @@ -9,7 +9,7 @@ import ( // to register handler for specific HTTP Methods inside the `Mux#Handle/HandleFunc`. // Usage: // mux := muxie.NewMux() -// mux.Handle("/user", muxie.Methods(). +// mux.Handle("/user/:id", muxie.Methods(). // Handle("GET", getUserHandler). // Handle("POST", saveUserHandler)) func Methods() *MethodHandler { @@ -18,17 +18,17 @@ func Methods() *MethodHandler { // // mux := muxie.NewMux() // - // 1. mux.Handle("/user", muxie.ByMethod("GET", getHandler).And/AndFunc("POST", postHandlerFunc)) + // 1. mux.Handle("/user/:id", muxie.ByMethod("GET", getHandler).And/AndFunc("POST", postHandlerFunc)) // - // 2. mux.Handle("/user", muxie.ByMethods{ + // 2. mux.Handle("/user/:id", muxie.ByMethods{ // "GET": getHandler, // "POST" http.HandlerFunc(postHandlerFunc), // }) <- the only downside of this is that // we lose the "Allow" header, which is not so important but it is RCF so we have to follow it. // - // 3. mux.Handle("/user", muxie.Method("GET", getUserHandler).Method("POST", saveUserHandler)) + // 3. mux.Handle("/user/:id", muxie.Method("GET", getUserHandler).Method("POST", saveUserHandler)) // - // 4. mux.Handle("/user", muxie.Methods(). + // 4. mux.Handle("/user/:id", muxie.Methods(). // Handle("GET", getHandler). // HandleFunc("POST", postHandler)) // @@ -49,8 +49,26 @@ type MethodHandler struct { // Handle adds a handler to be responsible for a specific HTTP Method. // Returns this MethodHandler for further calls. +// Usage: +// Handle("GET", myGetHandler).HandleFunc("DELETE", func(w http.ResponseWriter, r *http.Request){[...]}) +// Handle("POST, PUT", saveOrUpdateHandler) +// ^ can accept many methods for the same handler +// ^ methods should be separated by comma, comma following by a space or just space func (m *MethodHandler) Handle(method string, handler http.Handler) *MethodHandler { - method = strings.ToUpper(method) + multiMethods := strings.FieldsFunc(method, func(c rune) bool { + return c == ',' || c == ' ' + }) + + if len(multiMethods) > 1 { + for _, method := range multiMethods { + m.Handle(method, handler) + } + + return m + } + + method = strings.ToUpper(strings.TrimSpace(method)) + if m.methodsAllowedStr == "" { m.methodsAllowedStr = method } else { diff --git a/method_handler_test.go b/method_handler_test.go index a04aef8..20646e9 100644 --- a/method_handler_test.go +++ b/method_handler_test.go @@ -46,7 +46,6 @@ func TestMethodHandler(t *testing.T) { bodyEq("POST: save user with ID: 42\n") expect(t, http.MethodDelete, srv.URL+"/user/42").statusCode(http.StatusOK). bodyEq("DELETE: remove user with ID: 42\n") - expect(t, http.MethodPut, srv.URL+"/user/42").statusCode(http.StatusMethodNotAllowed). bodyEq("Method Not Allowed\n").headerEq("Allow", "GET, POST, DELETE") } diff --git a/mux_test.go b/mux_test.go index d0955f6..408ba91 100644 --- a/mux_test.go +++ b/mux_test.go @@ -1,6 +1,7 @@ package muxie import ( + "bytes" "fmt" "io/ioutil" "net/http" @@ -13,6 +14,27 @@ func expect(t *testing.T, method, url string) *testie { if err != nil { t.Fatal(err) } + + return testReq(t, req) +} + +func expectWithBody(t *testing.T, method, url string, body string, headers http.Header) *testie { + req, err := http.NewRequest(method, url, bytes.NewBufferString(body)) + if err != nil { + t.Fatal(err) + } + + if len(headers) > 0 { + req.Header = http.Header{} + for k, v := range headers { + req.Header[k] = v + } + } + + return testReq(t, req) +} + +func testReq(t *testing.T, req *http.Request) *testie { res, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) diff --git a/request_processor.go b/request_processor.go new file mode 100644 index 0000000..46cac85 --- /dev/null +++ b/request_processor.go @@ -0,0 +1,148 @@ +package muxie + +import ( + "bytes" + "encoding/json" + "encoding/xml" + "io/ioutil" + "net/http" +) + +var ( + Charset = "utf-8" + + JSON = &jsonProcessor{Prefix: nil, Indent: "", UnescapeHTML: false} + XML = &xmlProcessor{Indent: ""} +) + +func withCharset(cType string) string { + return cType + "; charset=" + Charset +} + +type Binder interface { + Bind(*http.Request, interface{}) error +} + +func Bind(r *http.Request, b Binder, ptrOut interface{}) error { + return b.Bind(r, ptrOut) +} + +type Dispatcher interface { + // no io.Writer because we need to set the headers here, + // Binder and Processor are only for HTTP. + Dispatch(http.ResponseWriter, interface{}) error +} + +func Dispatch(w http.ResponseWriter, d Dispatcher, v interface{}) error { + return d.Dispatch(w, v) +} + +type Processor interface { + Binder + Dispatcher +} + +var ( + newLineB byte = '\n' + // the html codes for unescaping + ltHex = []byte("\\u003c") + lt = []byte("<") + + gtHex = []byte("\\u003e") + gt = []byte(">") + + andHex = []byte("\\u0026") + and = []byte("&") +) + +type jsonProcessor struct { + Prefix []byte + Indent string + UnescapeHTML bool +} + +var _ Processor = (*jsonProcessor)(nil) + +func (p *jsonProcessor) Bind(r *http.Request, v interface{}) error { + b, err := ioutil.ReadAll(r.Body) + if err != nil { + return err + } + + return json.Unmarshal(b, v) +} + +func (p *jsonProcessor) Dispatch(w http.ResponseWriter, v interface{}) error { + var ( + result []byte + err error + ) + + if indent := p.Indent; indent != "" { + marshalIndent := json.MarshalIndent + + result, err = marshalIndent(v, "", indent) + result = append(result, newLineB) + } else { + marshal := json.Marshal + result, err = marshal(v) + } + + if err != nil { + return err + } + + if p.UnescapeHTML { + result = bytes.Replace(result, ltHex, lt, -1) + result = bytes.Replace(result, gtHex, gt, -1) + result = bytes.Replace(result, andHex, and, -1) + } + + if len(p.Prefix) > 0 { + result = append([]byte(p.Prefix), result...) + } + + w.Header().Set("Content-Type", withCharset("application/json")) + _, err = w.Write(result) + return err +} + +type xmlProcessor struct { + Indent string +} + +var _ Processor = (*xmlProcessor)(nil) + +func (p *xmlProcessor) Bind(r *http.Request, v interface{}) error { + b, err := ioutil.ReadAll(r.Body) + if err != nil { + return err + } + + return xml.Unmarshal(b, v) +} + +func (p *xmlProcessor) Dispatch(w http.ResponseWriter, v interface{}) error { + var ( + result []byte + err error + ) + + if indent := p.Indent; indent != "" { + marshalIndent := xml.MarshalIndent + + result, err = marshalIndent(v, "", indent) + result = append(result, newLineB) + } else { + marshal := xml.Marshal + result, err = marshal(v) + } + + if err != nil { + return err + } + + w.Header().Set("Content-Type", withCharset("text/xml")) + _, err = w.Write(result) + return err +} diff --git a/request_processor_test.go b/request_processor_test.go new file mode 100644 index 0000000..ef91c85 --- /dev/null +++ b/request_processor_test.go @@ -0,0 +1,63 @@ +package muxie + +import ( + "encoding/xml" + "fmt" + "net/http" + "net/http/httptest" + "testing" +) + +type person struct { + XMLName xml.Name `json:"-" xml:"person"` + Name string `json:"name" xml:"name,attr"` + Age int `json:"age" xml:"age,attr"` + Description string `json:"description" xml:"description"` +} + +func testProcessor(t *testing.T, p Processor, cType, tmplStrValue string) { + testValue := person{Name: "kataras", Age: 25, Description: "software engineer"} + testValueStr := fmt.Sprintf(tmplStrValue, testValue.Name, testValue.Age, testValue.Description) + + mux := NewMux() + mux.HandleFunc("/read", func(w http.ResponseWriter, r *http.Request) { + var v person + if err := Bind(r, p, &v); err != nil { + t.Fatal(err) + } + + if expected, got := v.Name, testValue.Name; expected != got { + t.Fatalf("expected name to be: '%s' but got: '%s'", expected, got) + } + if expected, got := v.Age, testValue.Age; expected != got { + t.Fatalf("expected age to be: '%d' but got: '%d'", expected, got) + } + + if expected, got := v.Description, testValue.Description; expected != got { + t.Fatalf("expected description to be: '%s' but got: '%s'", expected, got) + } + }) + mux.HandleFunc("/write", func(w http.ResponseWriter, r *http.Request) { + if err := Dispatch(w, p, testValue); err != nil { + t.Fatal(err) + } + }) + + srv := httptest.NewServer(mux) + defer srv.Close() + + expectWithBody(t, http.MethodGet, srv.URL+"/read", testValueStr, + http.Header{"Content-Type": []string{cType}}).statusCode(http.StatusOK) + + expect(t, http.MethodGet, srv.URL+"/write").statusCode(http.StatusOK). + headerEq("Content-Type", withCharset(cType)). + bodyEq(testValueStr) +} + +func TestJSON(t *testing.T) { + testProcessor(t, JSON, "application/json", `{"name":"%s","age":%d,"description":"%s"}`) +} + +func TestXML(t *testing.T) { + testProcessor(t, XML, "text/xml", `%s`) +}