diff --git a/.travis.yml b/.travis.yml
index 6abc0bd..d190fc0 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,6 +1,6 @@
language: go
go:
- - 1.12.4
+ - 1.12.5
- tip
matrix:
allow_failures:
diff --git a/README.md b/README.md
index 659f7d9..ffd359b 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
# pkg
-
+
[](https://travis-ci.org/go-playground/pkg)
[](https://coveralls.io/github/go-playground/pkg?branch=master)
[](https://godoc.org/github.com/go-playground/pkg)
diff --git a/go.mod b/go.mod
index dbe90d0..5abb911 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,8 @@
module github.com/go-playground/pkg
go 1.11
+
+require (
+ github.com/go-playground/form v3.1.4+incompatible
+ gopkg.in/go-playground/assert.v1 v1.2.1
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..c93aa69
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,4 @@
+github.com/go-playground/form v3.1.4+incompatible h1:lvKiHVxE2WvzDIoyMnWcjyiBxKt2+uFJyZcPYWsLnjI=
+github.com/go-playground/form v3.1.4+incompatible/go.mod h1:lhcKXfTuhRtIZCIKUeJ0b5F207aeQCPbZU09ScKjwWg=
+gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
+gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
diff --git a/net/http/accept_encodings.go b/net/http/accept_encodings.go
new file mode 100644
index 0000000..16d047d
--- /dev/null
+++ b/net/http/accept_encodings.go
@@ -0,0 +1,11 @@
+package httpext
+
+// Accept-Encoding values
+const (
+ Gzip string = "gzip"
+ Compress string = "compress"
+ Deflate string = "deflate"
+ Br string = "br"
+ Identity string = "identity"
+ Any string = "*"
+)
diff --git a/net/http/charset.go b/net/http/charset.go
new file mode 100644
index 0000000..bccffa0
--- /dev/null
+++ b/net/http/charset.go
@@ -0,0 +1,7 @@
+package httpext
+
+// Charset values
+const (
+ UTF8 string = "utf-8"
+ ISO88591 string = "iso-8859-1"
+)
diff --git a/net/http/form.go b/net/http/form.go
new file mode 100644
index 0000000..e1784ba
--- /dev/null
+++ b/net/http/form.go
@@ -0,0 +1,17 @@
+package httpext
+
+import (
+ "net/url"
+
+ "github.com/go-playground/form"
+)
+
+// FormDecoder is the type used for decoding a form for use
+type FormDecoder interface {
+ Decode(interface{}, url.Values) error
+}
+
+var (
+ // DefaultDecoder of this package, which is configurable
+ DefaultDecoder FormDecoder = form.NewDecoder()
+)
diff --git a/net/http/headers.go b/net/http/headers.go
new file mode 100644
index 0000000..e135732
--- /dev/null
+++ b/net/http/headers.go
@@ -0,0 +1,76 @@
+package httpext
+
+// HTTP Header keys
+const (
+ Age string = "Age"
+ AltSCV string = "Alt-Svc"
+ Accept string = "Accept"
+ AcceptCharset string = "Accept-Charset"
+ AcceptPatch string = "Accept-Patch"
+ AcceptRanges string = "Accept-Ranges"
+ AcceptedLanguage string = "Accept-Language"
+ AcceptEncoding string = "Accept-Encoding"
+ Authorization string = "Authorization"
+ CrossOriginResourcePolicy string = "Cross-Origin-Resource-Policy"
+ CacheControl string = "Cache-Control"
+ Connection string = "Connection"
+ ContentDisposition string = "Content-Disposition"
+ ContentEncoding string = "Content-Encoding"
+ ContentLength string = "Content-Length"
+ ContentType string = "Content-Type"
+ ContentLanguage string = "Content-Language"
+ ContentLocation string = "Content-Location"
+ ContentRange string = "Content-Range"
+ Date string = "Date"
+ DeltaBase string = "Delta-Base"
+ ETag string = "ETag"
+ Expires string = "Expires"
+ Host string = "Host"
+ IM string = "IM"
+ IfMatch string = "If-Match"
+ IfModifiedSince string = "If-Modified-Since"
+ IfNoneMatch string = "If-None-Match"
+ IfRange string = "If-Range"
+ IfUnmodifiedSince string = "If-Unmodified-Since"
+ KeepAlive string = "Keep-Alive"
+ LastModified string = "Last-Modified"
+ Link string = "Link"
+ Pragma string = "Pragma"
+ ProxyAuthenticate string = "Proxy-Authenticate"
+ ProxyAuthorization string = "Proxy-Authorization"
+ PublicKeyPins string = "Public-Key-Pins"
+ RetryAfter string = "Retry-After"
+ Referer string = "Referer"
+ Server string = "Server"
+ SetCookie string = "Set-Cookie"
+ StrictTransportSecurity string = "Strict-Transport-Security"
+ Trailer string = "Trailer"
+ TK string = "Tk"
+ TransferEncoding string = "Transfer-Encoding"
+ Location string = "Location"
+ Upgrade string = "Upgrade"
+ Vary string = "Vary"
+ Via string = "Via"
+ Warning string = "Warning"
+ WWWAuthenticate string = "WWW-Authenticate"
+ XForwardedFor string = "X-Forwarded-For"
+ XForwardedHost string = "X-Forwarded-Host"
+ XForwardedProto string = "X-Forwarded-Proto"
+ XRealIP string = "X-Real-Ip"
+ XContentTypeOptions string = "X-Content-Type-Options"
+ XFrameOptions string = "X-Frame-Options"
+ XXSSProtection string = "X-XSS-Protection"
+ XDNSPrefetchControl string = "X-DNS-Prefetch-Control"
+ Allow string = "Allow"
+ Origin string = "Origin"
+ AccessControlAllowOrigin string = "Access-Control-Allow-Origin"
+ AccessControlAllowCredentials string = "Access-Control-Allow-Credentials"
+ AccessControlAllowHeaders string = "Access-Control-Allow-Headers"
+ AccessControlAllowMethods string = "Access-Control-Allow-Methods"
+ AccessControlExposeHeaders string = "Access-Control-Expose-Headers"
+ AccessControlMaxAge string = "Access-Control-Max-Age"
+ AccessControlRequestHeaders string = "Access-Control-Request-Headers"
+ AccessControlRequestMethod string = "Access-Control-Request-Method"
+ TimingAllowOrigin string = "Timing-Allow-Origin"
+ UserAgent string = "User-Agent"
+)
diff --git a/net/http/helpers.go b/net/http/helpers.go
new file mode 100644
index 0000000..634bc49
--- /dev/null
+++ b/net/http/helpers.go
@@ -0,0 +1,296 @@
+package httpext
+
+import (
+ "compress/gzip"
+ "encoding/json"
+ "encoding/xml"
+ "io"
+ "mime"
+ "net"
+ "net/http"
+ "path/filepath"
+ "strings"
+
+ ioext "github.com/go-playground/pkg/io"
+)
+
+// QueryParamsOption represents the options for including query parameters during Decode helper functions
+type QueryParamsOption uint8
+
+const (
+ QueryParams QueryParamsOption = iota
+ NoQueryParams
+)
+
+var (
+ xmlHeaderBytes = []byte(xml.Header)
+)
+
+func detectContentType(filename string) string {
+ ext := strings.ToLower(filepath.Ext(filename))
+ if t := mime.TypeByExtension(ext); t != "" {
+ return t
+ }
+ switch ext {
+ case ".md":
+ return TextMarkdown
+ default:
+ return OctetStream
+ }
+}
+
+// AcceptedLanguages returns an array of accepted languages denoted by
+// the Accept-Language header sent by the browser
+func AcceptedLanguages(r *http.Request) (languages []string) {
+ accepted := r.Header.Get(AcceptedLanguage)
+ if accepted == "" {
+ return
+ }
+ options := strings.Split(accepted, ",")
+ l := len(options)
+ languages = make([]string, l)
+
+ for i := 0; i < l; i++ {
+ locale := strings.SplitN(options[i], ";", 2)
+ languages[i] = strings.Trim(locale[0], " ")
+ }
+ return
+}
+
+// Attachment is a helper method for returning an attachement file
+// to be downloaded, if you with to open inline see function Inline
+func Attachment(w http.ResponseWriter, r io.Reader, filename string) (err error) {
+ w.Header().Set(ContentDisposition, "attachment;filename="+filename)
+ w.Header().Set(ContentType, detectContentType(filename))
+ w.WriteHeader(http.StatusOK)
+ _, err = io.Copy(w, r)
+ return
+}
+
+// Inline is a helper method for returning a file inline to
+// be rendered/opened by the browser
+func Inline(w http.ResponseWriter, r io.Reader, filename string) (err error) {
+ w.Header().Set(ContentDisposition, "inline;filename="+filename)
+ w.Header().Set(ContentType, detectContentType(filename))
+ w.WriteHeader(http.StatusOK)
+ _, err = io.Copy(w, r)
+ return
+}
+
+// ClientIP implements a best effort algorithm to return the real client IP, it parses
+// X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy.
+func ClientIP(r *http.Request) (clientIP string) {
+ values := r.Header[XRealIP]
+ if len(values) > 0 {
+ clientIP = strings.TrimSpace(values[0])
+ if clientIP != "" {
+ return
+ }
+ }
+ if values = r.Header[XForwardedFor]; len(values) > 0 {
+ clientIP = values[0]
+ if index := strings.IndexByte(clientIP, ','); index >= 0 {
+ clientIP = clientIP[0:index]
+ }
+ clientIP = strings.TrimSpace(clientIP)
+ if clientIP != "" {
+ return
+ }
+ }
+ clientIP, _, _ = net.SplitHostPort(strings.TrimSpace(r.RemoteAddr))
+ return
+}
+
+// JSON marshals provided interface + returns JSON + status code
+func JSON(w http.ResponseWriter, status int, i interface{}) error {
+ enc := json.NewEncoder(w)
+ err := enc.Encode(i)
+ if err != nil {
+ return err
+ }
+ w.Header().Set(ContentType, ApplicationJSON)
+ w.WriteHeader(status)
+ return nil
+}
+
+// JSONBytes returns provided JSON response with status code
+func JSONBytes(w http.ResponseWriter, status int, b []byte) (err error) {
+ w.Header().Set(ContentType, ApplicationJSON)
+ w.WriteHeader(status)
+ _, err = w.Write(b)
+ return
+}
+
+// JSONP sends a JSONP response with status code and uses `callback` to construct
+// the JSONP payload.
+func JSONP(w http.ResponseWriter, status int, i interface{}, callback string) error {
+ b, err := json.Marshal(i)
+ if err != nil {
+ return err
+ }
+ w.Header().Set(ContentType, ApplicationJSON)
+ w.WriteHeader(status)
+
+ if _, err = w.Write([]byte(callback + "(")); err == nil {
+ if _, err = w.Write(b); err == nil {
+ _, err = w.Write([]byte(");"))
+ }
+ }
+ return err
+}
+
+// XML marshals provided interface + returns XML + status code
+func XML(w http.ResponseWriter, status int, i interface{}) error {
+ b, err := xml.Marshal(i)
+ if err != nil {
+ return err
+ }
+ w.Header().Set(ContentType, ApplicationXML)
+ w.WriteHeader(status)
+ if _, err = w.Write(xmlHeaderBytes); err == nil {
+ _, err = w.Write(b)
+ }
+ return err
+}
+
+// XMLBytes returns provided XML response with status code
+func XMLBytes(w http.ResponseWriter, status int, b []byte) (err error) {
+ w.Header().Set(ContentType, ApplicationXML)
+ w.WriteHeader(status)
+ if _, err = w.Write(xmlHeaderBytes); err == nil {
+ _, err = w.Write(b)
+ }
+ return
+}
+
+// DecodeForm parses the requests form data into the provided struct.
+//
+// The Content-Type and http method are not checked.
+//
+// NOTE: when QueryParamsOption=QueryParams the query params will be parsed and included eg. route /user?test=true 'test'
+// is added to parsed Form.
+func DecodeForm(r *http.Request, qp QueryParamsOption, v interface{}) (err error) {
+ if err = r.ParseForm(); err == nil {
+ switch qp {
+ case QueryParams:
+ err = DefaultDecoder.Decode(v, r.Form)
+ case NoQueryParams:
+ err = DefaultDecoder.Decode(v, r.PostForm)
+ }
+ }
+ return
+}
+
+// DecodeMultipartForm parses the requests form data into the provided struct.
+//
+// The Content-Type and http method are not checked.
+//
+// NOTE: when includeQueryParams=true query params will be parsed and included eg. route /user?test=true 'test'
+// is added to parsed MultipartForm.
+func DecodeMultipartForm(r *http.Request, qp QueryParamsOption, maxMemory int64, v interface{}) (err error) {
+ if err = r.ParseMultipartForm(maxMemory); err == nil {
+ switch qp {
+ case QueryParams:
+ err = DefaultDecoder.Decode(v, r.Form)
+ case NoQueryParams:
+ err = DefaultDecoder.Decode(v, r.MultipartForm.Value)
+ }
+ }
+ return
+}
+
+// DecodeJSON decodes the request body into the provided struct and limits the request size via
+// an ioext.LimitReader using the maxMemory param.
+//
+// The Content-Type e.g. "application/json" and http method are not checked.
+//
+// NOTE: when includeQueryParams=true query params will be parsed and included eg. route /user?test=true 'test'
+// is added to parsed JSON and replaces any value that may have been present
+func DecodeJSON(r *http.Request, qp QueryParamsOption, maxMemory int64, v interface{}) (err error) {
+ var body io.Reader = r.Body
+ if encoding := r.Header.Get(ContentEncoding); encoding == Gzip {
+ var gzr *gzip.Reader
+ gzr, err = gzip.NewReader(r.Body)
+ if err != nil {
+ return
+ }
+ defer func() {
+ _ = gzr.Close()
+ }()
+ body = gzr
+ }
+ err = json.NewDecoder(ioext.LimitReader(body, maxMemory)).Decode(v)
+ if qp == QueryParams && err == nil {
+ err = DecodeQueryParams(r, v)
+ }
+ return
+}
+
+// DecodeXML decodes the request body into the provided struct and limits the request size via
+// an ioext.LimitReader using the maxMemory param.
+//
+// The Content-Type e.g. "application/xml" and http method are not checked.
+//
+// NOTE: when includeQueryParams=true query params will be parsed and included eg. route /user?test=true 'test'
+// is added to parsed XML and replaces any value that may have been present
+func DecodeXML(r *http.Request, qp QueryParamsOption, maxMemory int64, v interface{}) (err error) {
+ var body io.Reader = r.Body
+ if encoding := r.Header.Get(ContentEncoding); encoding == Gzip {
+ var gzr *gzip.Reader
+ gzr, err = gzip.NewReader(r.Body)
+ if err != nil {
+ return
+ }
+ defer func() {
+ _ = gzr.Close()
+ }()
+ body = gzr
+ }
+ err = xml.NewDecoder(ioext.LimitReader(body, maxMemory)).Decode(v)
+ if qp == QueryParams && err == nil {
+ err = DecodeQueryParams(r, v)
+ }
+ return
+}
+
+// DecodeQueryParams takes the URL Query params flag.
+func DecodeQueryParams(r *http.Request, v interface{}) (err error) {
+ err = DefaultDecoder.Decode(v, r.URL.Query())
+ return
+}
+
+const (
+ nakedApplicationJSON string = "application/json"
+ nakedApplicationXML string = "application/xml"
+)
+
+// Decode takes the request and attempts to discover it's content type via
+// the http headers and then decode the request body into the provided struct.
+// Example if header was "application/json" would decode using
+// json.NewDecoder(ioext.LimitReader(r.Body, maxMemory)).Decode(v).
+//
+// This default to parsing query params if includeQueryParams=true and no other content type matches.
+//
+// NOTE: when includeQueryParams=true query params will be parsed and included eg. route /user?test=true 'test'
+// is added to parsed XML and replaces any value that may have been present
+func Decode(r *http.Request, qp QueryParamsOption, maxMemory int64, v interface{}) (err error) {
+ typ := r.Header.Get(ContentType)
+ if idx := strings.Index(typ, ";"); idx != -1 {
+ typ = typ[:idx]
+ }
+ switch typ {
+ case nakedApplicationJSON:
+ err = DecodeJSON(r, qp, maxMemory, v)
+ case nakedApplicationXML:
+ err = DecodeXML(r, qp, maxMemory, v)
+ case ApplicationForm:
+ err = DecodeForm(r, qp, v)
+ case MultipartForm:
+ err = DecodeMultipartForm(r, qp, maxMemory, v)
+ default:
+ if qp == QueryParams {
+ err = DecodeQueryParams(r, v)
+ }
+ }
+ return
+}
diff --git a/net/http/helpers_test.go b/net/http/helpers_test.go
new file mode 100644
index 0000000..1d2499b
--- /dev/null
+++ b/net/http/helpers_test.go
@@ -0,0 +1,439 @@
+package httpext
+
+import (
+ "bytes"
+ "compress/gzip"
+ "encoding/json"
+ "encoding/xml"
+ "mime/multipart"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "os"
+ "strings"
+ "testing"
+
+ . "gopkg.in/go-playground/assert.v1"
+)
+
+func TestAcceptedLanguages(t *testing.T) {
+ req, _ := http.NewRequest("POST", "/", nil)
+ req.Header.Set(AcceptedLanguage, "da, en-GB;q=0.8, en;q=0.7")
+
+ languages := AcceptedLanguages(req)
+
+ Equal(t, languages[0], "da")
+ Equal(t, languages[1], "en-GB")
+ Equal(t, languages[2], "en")
+
+ req.Header.Del(AcceptedLanguage)
+
+ languages = AcceptedLanguages(req)
+ Equal(t, len(languages), 0)
+
+ req.Header.Set(AcceptedLanguage, "")
+ languages = AcceptedLanguages(req)
+ Equal(t, len(languages), 0)
+}
+
+func TestAttachment(t *testing.T) {
+ mux := http.NewServeMux()
+ mux.HandleFunc("/dl", func(w http.ResponseWriter, r *http.Request) {
+ f, _ := os.Open("../../README.md")
+ if err := Attachment(w, f, "README.md"); err != nil {
+ panic(err)
+ }
+ })
+ mux.HandleFunc("/dl-unknown-type", func(w http.ResponseWriter, r *http.Request) {
+ f, _ := os.Open("../../README.md")
+ if err := Attachment(w, f, "readme"); err != nil {
+ panic(err)
+ }
+ })
+ mux.HandleFunc("/dl-fake-png", func(w http.ResponseWriter, r *http.Request) {
+ f, _ := os.Open("../../README.md")
+ if err := Attachment(w, f, "logo.png"); err != nil {
+ panic(err)
+ }
+ })
+
+ tests := []struct {
+ name string
+ code int
+ disposition string
+ typ string
+ url string
+ }{
+ {
+ code: http.StatusOK,
+ disposition: "attachment;filename=README.md",
+ typ: TextMarkdown,
+ url: "/dl",
+ },
+ {
+ code: http.StatusOK,
+ disposition: "attachment;filename=readme",
+ typ: OctetStream,
+ url: "/dl-unknown-type",
+ },
+ {
+ code: http.StatusOK,
+ disposition: "attachment;filename=logo.png",
+ typ: ImagePNG,
+ url: "/dl-fake-png",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ req, err := http.NewRequest(http.MethodGet, tt.url, nil)
+ Equal(t, err, nil)
+
+ w := httptest.NewRecorder()
+ mux.ServeHTTP(w, req)
+
+ if tt.code != w.Code {
+ t.Errorf("Status Code = %d, want %d", w.Code, tt.code)
+ }
+ if tt.disposition != w.Header().Get(ContentDisposition) {
+ t.Errorf("Content Disaposition = %s, want %s", w.Header().Get(ContentDisposition), tt.disposition)
+ }
+ if tt.typ != w.Header().Get(ContentType) {
+ t.Errorf("Content Type = %s, want %s", w.Header().Get(ContentType), tt.typ)
+ }
+ })
+ }
+}
+
+func TestInline(t *testing.T) {
+ mux := http.NewServeMux()
+ mux.HandleFunc("/dl-inline", func(w http.ResponseWriter, r *http.Request) {
+ f, _ := os.Open("../../README.md")
+ if err := Inline(w, f, "README.md"); err != nil {
+ panic(err)
+ }
+ })
+ mux.HandleFunc("/dl-unknown-type-inline", func(w http.ResponseWriter, r *http.Request) {
+ f, _ := os.Open("../../README.md")
+ if err := Inline(w, f, "readme"); err != nil {
+ panic(err)
+ }
+ })
+
+ tests := []struct {
+ name string
+ code int
+ disposition string
+ typ string
+ url string
+ }{
+ {
+ code: http.StatusOK,
+ disposition: "inline;filename=README.md",
+ typ: TextMarkdown,
+ url: "/dl-inline",
+ },
+ {
+ code: http.StatusOK,
+ disposition: "inline;filename=readme",
+ typ: OctetStream,
+ url: "/dl-unknown-type-inline",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ req, err := http.NewRequest(http.MethodGet, tt.url, nil)
+ Equal(t, err, nil)
+
+ w := httptest.NewRecorder()
+ mux.ServeHTTP(w, req)
+
+ if tt.code != w.Code {
+ t.Errorf("Status Code = %d, want %d", w.Code, tt.code)
+ }
+ if tt.disposition != w.Header().Get(ContentDisposition) {
+ t.Errorf("Content Disaposition = %s, want %s", w.Header().Get(ContentDisposition), tt.disposition)
+ }
+ if tt.typ != w.Header().Get(ContentType) {
+ t.Errorf("Content Type = %s, want %s", w.Header().Get(ContentType), tt.typ)
+ }
+ })
+ }
+}
+
+func TestClientIP(t *testing.T) {
+ req, _ := http.NewRequest("POST", "/", nil)
+ req.Header.Set("X-Real-IP", " 10.10.10.10 ")
+ req.Header.Set("X-Forwarded-For", " 20.20.20.20, 30.30.30.30")
+ req.RemoteAddr = " 40.40.40.40:42123 "
+
+ Equal(t, ClientIP(req), "10.10.10.10")
+
+ req.Header.Del("X-Real-IP")
+ Equal(t, ClientIP(req), "20.20.20.20")
+
+ req.Header.Set("X-Forwarded-For", "30.30.30.30 ")
+ Equal(t, ClientIP(req), "30.30.30.30")
+
+ req.Header.Del("X-Forwarded-For")
+ Equal(t, ClientIP(req), "40.40.40.40")
+}
+
+func TestJSON(t *testing.T) {
+ w := httptest.NewRecorder()
+ type test struct {
+ Field string `json:"field"`
+ }
+ tst := test{Field: "myfield"}
+ b, err := json.Marshal(tst)
+ Equal(t, err, nil)
+
+ err = JSON(w, http.StatusOK, tst)
+ Equal(t, err, nil)
+ Equal(t, w.Header().Get(ContentType), ApplicationJSON)
+ Equal(t, w.Body.Bytes(), append([]byte(b), '\n'))
+
+ err = JSON(w, http.StatusOK, func() {})
+ NotEqual(t, err, nil)
+}
+
+func TestJSONBytes(t *testing.T) {
+ w := httptest.NewRecorder()
+ type test struct {
+ Field string `json:"field"`
+ }
+ tst := test{Field: "myfield"}
+ b, err := json.Marshal(tst)
+ Equal(t, err, nil)
+
+ err = JSONBytes(w, http.StatusOK, b)
+ Equal(t, err, nil)
+ Equal(t, w.Header().Get(ContentType), ApplicationJSON)
+ Equal(t, w.Body.Bytes(), []byte(b))
+}
+
+func TestJSONP(t *testing.T) {
+ callbackFunc := "CallbackFunc"
+ w := httptest.NewRecorder()
+ type test struct {
+ Field string `json:"field"`
+ }
+ tst := test{Field: "myfield"}
+ err := JSONP(w, http.StatusOK, tst, callbackFunc)
+ Equal(t, err, nil)
+ Equal(t, w.Header().Get(ContentType), ApplicationJSON)
+
+ err = JSON(w, http.StatusOK, func() {})
+ NotEqual(t, err, nil)
+}
+
+func TestXML(t *testing.T) {
+ w := httptest.NewRecorder()
+ type zombie struct {
+ ID int `json:"id" xml:"id"`
+ Name string `json:"name" xml:"name"`
+ }
+ tst := zombie{1, "Patient Zero"}
+ xmlData := `1Patient Zero`
+ err := XML(w, http.StatusOK, tst)
+ Equal(t, err, nil)
+ Equal(t, w.Header().Get(ContentType), ApplicationXML)
+ Equal(t, w.Body.Bytes(), []byte(xml.Header+xmlData))
+
+ err = JSON(w, http.StatusOK, func() {})
+ NotEqual(t, err, nil)
+}
+
+func TestXMLBytes(t *testing.T) {
+ xmlData := `1Patient Zero`
+ w := httptest.NewRecorder()
+ err := XMLBytes(w, http.StatusOK, []byte(xmlData))
+ Equal(t, err, nil)
+ Equal(t, w.Header().Get(ContentType), ApplicationXML)
+ Equal(t, w.Body.Bytes(), []byte(xml.Header+xmlData))
+}
+
+func TestDecode(t *testing.T) {
+ type TestStruct struct {
+ ID int `form:"id"`
+ Posted string
+ MultiPartPosted string
+ }
+
+ test := new(TestStruct)
+
+ mux := http.NewServeMux()
+ mux.HandleFunc("/decode-noquery", func(w http.ResponseWriter, r *http.Request) {
+ err := Decode(r, NoQueryParams, 16<<10, test)
+ Equal(t, err, nil)
+ })
+ mux.HandleFunc("/decode-query", func(w http.ResponseWriter, r *http.Request) {
+ err := Decode(r, QueryParams, 16<<10, test)
+ Equal(t, err, nil)
+ })
+
+ // test query params
+ r, _ := http.NewRequest(http.MethodGet, "/decode-query?id=5", nil)
+ w := httptest.NewRecorder()
+ mux.ServeHTTP(w, r)
+ Equal(t, w.Code, http.StatusOK)
+ Equal(t, test.ID, 5)
+ Equal(t, test.Posted, "")
+ Equal(t, test.MultiPartPosted, "")
+
+ // test Form decode
+ form := url.Values{}
+ form.Add("Posted", "value")
+
+ test = new(TestStruct)
+ r, _ = http.NewRequest(http.MethodPost, "/decode-query?id=13", strings.NewReader(form.Encode()))
+ r.Header.Set(ContentType, ApplicationForm)
+ w = httptest.NewRecorder()
+ mux.ServeHTTP(w, r)
+ Equal(t, w.Code, http.StatusOK)
+ Equal(t, test.ID, 13)
+ Equal(t, test.Posted, "value")
+ Equal(t, test.MultiPartPosted, "")
+
+ test = new(TestStruct)
+ r, _ = http.NewRequest(http.MethodPost, "/decode-noquery?id=14", strings.NewReader(form.Encode()))
+ r.Header.Set(ContentType, ApplicationForm)
+ w = httptest.NewRecorder()
+ mux.ServeHTTP(w, r)
+ Equal(t, w.Code, http.StatusOK)
+ Equal(t, test.ID, 0)
+ Equal(t, test.Posted, "value")
+ Equal(t, test.MultiPartPosted, "")
+
+ // test MultipartForm
+ body := &bytes.Buffer{}
+ writer := multipart.NewWriter(body)
+
+ err := writer.WriteField("MultiPartPosted", "value")
+ Equal(t, err, nil)
+
+ // Don't forget to close the multipart writer.
+ // If you don't close it, your request will be missing the terminating boundary.
+ err = writer.Close()
+ Equal(t, err, nil)
+
+ test = new(TestStruct)
+ r, _ = http.NewRequest(http.MethodPost, "/decode-query?id=12", body)
+ r.Header.Set(ContentType, writer.FormDataContentType())
+ w = httptest.NewRecorder()
+ mux.ServeHTTP(w, r)
+ Equal(t, w.Code, http.StatusOK)
+ Equal(t, test.ID, 12)
+ Equal(t, test.Posted, "")
+ Equal(t, test.MultiPartPosted, "value")
+
+ body = &bytes.Buffer{}
+ writer = multipart.NewWriter(body)
+
+ err = writer.WriteField("MultiPartPosted", "value")
+ Equal(t, err, nil)
+
+ // Don't forget to close the multipart writer.
+ // If you don't close it, your request will be missing the terminating boundary.
+ err = writer.Close()
+ Equal(t, err, nil)
+
+ test = new(TestStruct)
+ r, _ = http.NewRequest(http.MethodPost, "/decode-noquery?id=13", body)
+ r.Header.Set(ContentType, writer.FormDataContentType())
+ w = httptest.NewRecorder()
+ mux.ServeHTTP(w, r)
+ Equal(t, w.Code, http.StatusOK)
+ Equal(t, test.ID, 0)
+ Equal(t, test.Posted, "")
+ Equal(t, test.MultiPartPosted, "value")
+
+ // test JSON
+ jsonBody := `{"ID":13,"Posted":"value","MultiPartPosted":"value"}`
+ test = new(TestStruct)
+ r, _ = http.NewRequest(http.MethodPost, "/decode-query?id=13", strings.NewReader(jsonBody))
+ r.Header.Set(ContentType, ApplicationJSON)
+ w = httptest.NewRecorder()
+ mux.ServeHTTP(w, r)
+ Equal(t, w.Code, http.StatusOK)
+ Equal(t, test.ID, 13)
+ Equal(t, test.Posted, "value")
+ Equal(t, test.MultiPartPosted, "value")
+
+ var buff bytes.Buffer
+ gzw := gzip.NewWriter(&buff)
+ defer func() {
+ _ = gzw.Close()
+ }()
+ _, err = gzw.Write([]byte(jsonBody))
+ Equal(t, err, nil)
+
+ err = gzw.Close()
+ Equal(t, err, nil)
+
+ test = new(TestStruct)
+ r, _ = http.NewRequest(http.MethodPost, "/decode-query?id=14", &buff)
+ r.Header.Set(ContentType, ApplicationJSON)
+ r.Header.Set(ContentEncoding, Gzip)
+ w = httptest.NewRecorder()
+ mux.ServeHTTP(w, r)
+ Equal(t, w.Code, http.StatusOK)
+ Equal(t, test.ID, 14)
+ Equal(t, test.Posted, "value")
+ Equal(t, test.MultiPartPosted, "value")
+
+ test = new(TestStruct)
+ r, _ = http.NewRequest(http.MethodPost, "/decode-noquery?id=14", strings.NewReader(jsonBody))
+ r.Header.Set(ContentType, ApplicationJSON)
+ w = httptest.NewRecorder()
+ mux.ServeHTTP(w, r)
+ Equal(t, w.Code, http.StatusOK)
+ Equal(t, test.ID, 13)
+ Equal(t, test.Posted, "value")
+ Equal(t, test.MultiPartPosted, "value")
+
+ // test XML
+ xmlBody := `13valuevalue`
+ test = new(TestStruct)
+ r, _ = http.NewRequest(http.MethodPost, "/decode-noquery?id=14", strings.NewReader(xmlBody))
+ r.Header.Set(ContentType, ApplicationXML)
+ w = httptest.NewRecorder()
+ mux.ServeHTTP(w, r)
+ Equal(t, w.Code, http.StatusOK)
+ Equal(t, test.ID, 13)
+ Equal(t, test.Posted, "value")
+ Equal(t, test.MultiPartPosted, "value")
+
+ test = new(TestStruct)
+ r, _ = http.NewRequest(http.MethodPost, "/decode-query?id=14", strings.NewReader(xmlBody))
+ r.Header.Set(ContentType, ApplicationXML)
+ w = httptest.NewRecorder()
+ mux.ServeHTTP(w, r)
+ Equal(t, w.Code, http.StatusOK)
+ Equal(t, test.ID, 14)
+ Equal(t, test.Posted, "value")
+ Equal(t, test.MultiPartPosted, "value")
+
+ buff.Reset()
+ gzw = gzip.NewWriter(&buff)
+ defer func() {
+ _ = gzw.Close()
+ }()
+ _, err = gzw.Write([]byte(xmlBody))
+ Equal(t, err, nil)
+
+ err = gzw.Close()
+ Equal(t, err, nil)
+
+ test = new(TestStruct)
+ r, _ = http.NewRequest(http.MethodPost, "/decode-noquery?id=14", &buff)
+ r.Header.Set(ContentType, ApplicationXML)
+ r.Header.Set(ContentEncoding, Gzip)
+ w = httptest.NewRecorder()
+ mux.ServeHTTP(w, r)
+ Equal(t, w.Code, http.StatusOK)
+ Equal(t, test.ID, 13)
+ Equal(t, test.Posted, "value")
+ Equal(t, test.MultiPartPosted, "value")
+}
diff --git a/net/http/mime_types.go b/net/http/mime_types.go
new file mode 100644
index 0000000..2e60a50
--- /dev/null
+++ b/net/http/mime_types.go
@@ -0,0 +1,27 @@
+package httpext
+
+const (
+ charsetUTF8 = "; charset=" + UTF8
+)
+
+// Mime Type values for the Content-Type HTTP header
+const (
+ ApplicationJSON string = "application/json" + charsetUTF8
+ ApplicationJavaScript string = "application/javascript"
+ ApplicationXML string = "application/xml" + charsetUTF8
+ ApplicationForm string = "application/x-www-form-urlencoded"
+ ApplicationProtobuf string = "application/protobuf"
+ ApplicationMsgpack string = "application/msgpack"
+ ApplicationWasm string = "application/wasm"
+ ApplicationPDF string = "application/pdf"
+ TextHTML string = "text/html" + charsetUTF8
+ TextPlain string = "text/plain" + charsetUTF8
+ TextMarkdown string = "text/markdown" + charsetUTF8
+ TextCSS string = "text/css" + charsetUTF8
+ ImagePNG string = "image/png"
+ ImageGIF string = "image/gif"
+ ImageSVG string = "image/svg+xml"
+ ImageJPEG string = "image/jpeg"
+ MultipartForm string = "multipart/form-data"
+ OctetStream string = "application/octet-stream"
+)
diff --git a/net/http/quality_value.go b/net/http/quality_value.go
new file mode 100644
index 0000000..59243e6
--- /dev/null
+++ b/net/http/quality_value.go
@@ -0,0 +1,20 @@
+package httpext
+
+import "fmt"
+
+const (
+ // QualityValueFormat is a format string helper for Quality Values
+ QualityValueFormat = "%s;q=%1.3g"
+)
+
+// QualityValue accepts a value to add/concatenate a quality value to and
+// the quality value itself.
+func QualityValue(v string, qv float32) string {
+ if qv > 1 {
+ qv = 1 // highest possible value
+ }
+ if qv < 0.001 {
+ qv = 0.001 // lowest possible value
+ }
+ return fmt.Sprintf(QualityValueFormat, v, qv)
+}
diff --git a/net/http/quality_value_test.go b/net/http/quality_value_test.go
new file mode 100644
index 0000000..353e0c1
--- /dev/null
+++ b/net/http/quality_value_test.go
@@ -0,0 +1,43 @@
+package httpext
+
+import "testing"
+
+func TestQualityValue(t *testing.T) {
+ type args struct {
+ v string
+ qv float32
+ }
+ tests := []struct {
+ name string
+ args args
+ want string
+ }{
+ {
+ name: "in-range",
+ args: args{v: "test", qv: 0.5},
+ want: "test;q=0.5",
+ },
+ {
+ name: "in-range-trailing-zeros",
+ args: args{v: "test", qv: 0.500},
+ want: "test;q=0.5",
+ },
+ {
+ name: "greater-than-range",
+ args: args{v: "test", qv: 1.500},
+ want: "test;q=1",
+ },
+ {
+ name: "less-than-range",
+ args: args{v: "test", qv: 0.0000001},
+ want: "test;q=0.001",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := QualityValue(tt.args.v, tt.args.qv); got != tt.want {
+ t.Errorf("QualityValue() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}