From 835ccc36614f2f7536e8d405373491b4eea64cd7 Mon Sep 17 00:00:00 2001 From: Lukas Date: Thu, 20 Apr 2023 17:40:13 +0200 Subject: [PATCH 1/3] first draft of encoder/decoder interface --- decoder.go | 34 +++++++++++++++++++++----- responder.go | 69 ++++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 87 insertions(+), 16 deletions(-) diff --git a/decoder.go b/decoder.go index 883b2f8..8115dbe 100644 --- a/decoder.go +++ b/decoder.go @@ -11,6 +11,22 @@ import ( "github.com/ajg/form" ) +// Decoder is a generic interface to decode arbitrary data from a reader `r` to +// a value `v`. +type Decoder interface { + Decode(r io.Reader, req *http.Request, v interface{}) error +} + +// Package-level variables for decoding the supported formats. They are set to +// our default implementations. By setting render.Decode{JSON,XML,Form} you can +// customize Decoding (e.g. you might want to configure the JSON-decoder) +// TODO documentation +var ( + DecoderJSON Decoder = DecodeJSON{} + DecoderXML Decoder = DecodeXML{} + DecoderForm Decoder = DecodeForm{} +) + // Decode is a package-level variable set to our default Decoder. We do this // because it allows you to set render.Decode to another function with the // same function signature, while also utilizing the render.Decoder() function @@ -26,11 +42,11 @@ func DefaultDecoder(r *http.Request, v interface{}) error { switch GetRequestContentType(r) { case ContentTypeJSON: - err = DecodeJSON(r.Body, v) + err = DecoderJSON.Decode(r.Body, r, v) case ContentTypeXML: - err = DecodeXML(r.Body, v) + err = DecoderXML.Decode(r.Body, r, v) case ContentTypeForm: - err = DecodeForm(r.Body, v) + err = DecoderForm.Decode(r.Body, r, v) default: err = errors.New("render: unable to automatically decode the request content type") } @@ -38,20 +54,26 @@ func DefaultDecoder(r *http.Request, v interface{}) error { return err } +type DecodeJSON struct{} + // DecodeJSON decodes a given reader into an interface using the json decoder. -func DecodeJSON(r io.Reader, v interface{}) error { +func (DecodeJSON) Decode(r io.Reader, req *http.Request, v interface{}) error { defer io.Copy(ioutil.Discard, r) //nolint:errcheck return json.NewDecoder(r).Decode(v) } +type DecodeXML struct{} + // DecodeXML decodes a given reader into an interface using the xml decoder. -func DecodeXML(r io.Reader, v interface{}) error { +func (DecodeXML) Decode(r io.Reader, req *http.Request, v interface{}) error { defer io.Copy(ioutil.Discard, r) //nolint:errcheck return xml.NewDecoder(r).Decode(v) } +type DecodeForm struct{} + // DecodeForm decodes a given reader into an interface using the form decoder. -func DecodeForm(r io.Reader, v interface{}) error { +func (DecodeForm) Decode(r io.Reader, req *http.Request, v interface{}) error { decoder := form.NewDecoder(r) //nolint:errcheck return decoder.Decode(v) } diff --git a/responder.go b/responder.go index f38807d..12fbf7e 100644 --- a/responder.go +++ b/responder.go @@ -5,11 +5,31 @@ import ( "context" "encoding/json" "encoding/xml" + "errors" "fmt" "net/http" "reflect" ) +var ErrInvalidType error = errors.New("Invalid Type passed") + +// Encoder is a generic interface to encode arbitrary data from value `v` to a +// reader `r` +type Encoder interface { + Encode(w http.ResponseWriter, req *http.Request, v interface{}) error +} + +// Package-level variables for encoding the supported formats. They are set to +// our default implementations. By setting render.Encode{JSON,XML,Form} you can +// customize Encoding (e.g. you might want to configure the JSON-encoder) +// TODO document type constraints +var ( + EncoderJSON Encoder = EncodeJSON{} + EncoderXML Encoder = EncodeXML{} + EncoderData Encoder = EncodeData{} + EncoderPlainText Encoder = EncodePlainText{} +) + // M is a convenience alias for quickly building a map structure that is going // out to a responder. Just a short-hand. type M map[string]interface{} @@ -51,52 +71,77 @@ func DefaultResponder(w http.ResponseWriter, r *http.Request, v interface{}) { // Format response based on request Accept header. switch GetAcceptedContentType(r) { case ContentTypeJSON: - JSON(w, r, v) + EncoderJSON.Encode(w, r, v) case ContentTypeXML: - XML(w, r, v) + EncoderXML.Encode(w, r, v) default: - JSON(w, r, v) + EncoderJSON.Encode(w, r, v) } } +type EncodePlainText struct{} + // PlainText writes a string to the response, setting the Content-Type as // text/plain. -func PlainText(w http.ResponseWriter, r *http.Request, v string) { +func (EncodePlainText) Encode(w http.ResponseWriter, r *http.Request, vi interface{}) error { + v, ok := vi.(string) + if !ok { + return ErrInvalidType + } w.Header().Set("Content-Type", "text/plain; charset=utf-8") if status, ok := r.Context().Value(StatusCtxKey).(int); ok { w.WriteHeader(status) } w.Write([]byte(v)) //nolint:errcheck + return nil } +// TODO backwards compatible +type EncodeData struct{} + // Data writes raw bytes to the response, setting the Content-Type as // application/octet-stream. -func Data(w http.ResponseWriter, r *http.Request, v []byte) { +func (EncodeData) Encode(w http.ResponseWriter, r *http.Request, vi interface{}) error { + v, ok := vi.([]byte) + if !ok { + return ErrInvalidType + } w.Header().Set("Content-Type", "application/octet-stream") if status, ok := r.Context().Value(StatusCtxKey).(int); ok { w.WriteHeader(status) } w.Write(v) //nolint:errcheck + return nil } +// TODO backwards compatible +type EncodeHTML struct{} + // HTML writes a string to the response, setting the Content-Type as text/html. -func HTML(w http.ResponseWriter, r *http.Request, v string) { +func (EncodeHTML) Encode(w http.ResponseWriter, r *http.Request, vi interface{}) error { + v, ok := vi.(string) + if !ok { + return ErrInvalidType + } w.Header().Set("Content-Type", "text/html; charset=utf-8") if status, ok := r.Context().Value(StatusCtxKey).(int); ok { w.WriteHeader(status) } w.Write([]byte(v)) //nolint:errcheck + return nil } +type EncodeJSON struct{} + // JSON marshals 'v' to JSON, automatically escaping HTML and setting the // Content-Type as application/json. -func JSON(w http.ResponseWriter, r *http.Request, v interface{}) { +func (EncodeJSON) Encode(w http.ResponseWriter, r *http.Request, v interface{}) error { buf := &bytes.Buffer{} enc := json.NewEncoder(buf) enc.SetEscapeHTML(true) if err := enc.Encode(v); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) - return + return err } w.Header().Set("Content-Type", "application/json") @@ -104,16 +149,19 @@ func JSON(w http.ResponseWriter, r *http.Request, v interface{}) { w.WriteHeader(status) } w.Write(buf.Bytes()) //nolint:errcheck + return nil } +type EncodeXML struct{} + // XML marshals 'v' to XML, setting the Content-Type as application/xml. It // will automatically prepend a generic XML header (see encoding/xml.Header) if // one is not found in the first 100 bytes of 'v'. -func XML(w http.ResponseWriter, r *http.Request, v interface{}) { +func (EncodeXML) Encode(w http.ResponseWriter, r *http.Request, v interface{}) error { b, err := xml.Marshal(v) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) - return + return err } w.Header().Set("Content-Type", "application/xml; charset=utf-8") @@ -132,6 +180,7 @@ func XML(w http.ResponseWriter, r *http.Request, v interface{}) { } w.Write(b) //nolint:errcheck + return nil } // NoContent returns a HTTP 204 "No Content" response. From 41d5e04bcc893a007537e234797f8fcf06565d02 Mon Sep 17 00:00:00 2001 From: Lukas Date: Sat, 22 Apr 2023 18:42:19 +0200 Subject: [PATCH 2/3] revert removing the old functions but mark them as deprecated --- decoder.go | 43 +++++++++++++++++------ responder.go | 99 +++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 127 insertions(+), 15 deletions(-) diff --git a/decoder.go b/decoder.go index 8115dbe..2891794 100644 --- a/decoder.go +++ b/decoder.go @@ -20,11 +20,10 @@ type Decoder interface { // Package-level variables for decoding the supported formats. They are set to // our default implementations. By setting render.Decode{JSON,XML,Form} you can // customize Decoding (e.g. you might want to configure the JSON-decoder) -// TODO documentation var ( - DecoderJSON Decoder = DecodeJSON{} - DecoderXML Decoder = DecodeXML{} - DecoderForm Decoder = DecodeForm{} + DecoderJSON Decoder = DecodeJSONInter{} + DecoderXML Decoder = DecodeXMLInter{} + DecoderForm Decoder = DecodeFormInter{} ) // Decode is a package-level variable set to our default Decoder. We do this @@ -54,26 +53,50 @@ func DefaultDecoder(r *http.Request, v interface{}) error { return err } -type DecodeJSON struct{} +type DecodeJSONInter struct{} // DecodeJSON decodes a given reader into an interface using the json decoder. -func (DecodeJSON) Decode(r io.Reader, req *http.Request, v interface{}) error { +func (DecodeJSONInter) Decode(r io.Reader, req *http.Request, v interface{}) error { defer io.Copy(ioutil.Discard, r) //nolint:errcheck return json.NewDecoder(r).Decode(v) } -type DecodeXML struct{} +// DecodeJSON decodes a given reader into an interface using the json decoder. +// +// Deprecated: DecoderJSON.Decode() should be used. +func DecodeJSON(r io.Reader, v interface{}) error { + defer io.Copy(ioutil.Discard, r) //nolint:errcheck + return json.NewDecoder(r).Decode(v) +} + +type DecodeXMLInter struct{} // DecodeXML decodes a given reader into an interface using the xml decoder. -func (DecodeXML) Decode(r io.Reader, req *http.Request, v interface{}) error { +func (DecodeXMLInter) Decode(r io.Reader, req *http.Request, v interface{}) error { defer io.Copy(ioutil.Discard, r) //nolint:errcheck return xml.NewDecoder(r).Decode(v) } -type DecodeForm struct{} +// DecodeXML decodes a given reader into an interface using the xml decoder. +// +// Deprecated: DecoderXML.Decode() should be used. +func DecodeXML(r io.Reader, v interface{}) error { + defer io.Copy(ioutil.Discard, r) //nolint:errcheck + return xml.NewDecoder(r).Decode(v) +} + +type DecodeFormInter struct{} + +// DecodeForm decodes a given reader into an interface using the form decoder. +func (DecodeFormInter) Decode(r io.Reader, req *http.Request, v interface{}) error { + decoder := form.NewDecoder(r) //nolint:errcheck + return decoder.Decode(v) +} // DecodeForm decodes a given reader into an interface using the form decoder. -func (DecodeForm) Decode(r io.Reader, req *http.Request, v interface{}) error { +// +// Deprecated: DecoderForm.Decode() should be used. +func DecodeForm(r io.Reader, v interface{}) error { decoder := form.NewDecoder(r) //nolint:errcheck return decoder.Decode(v) } diff --git a/responder.go b/responder.go index 12fbf7e..17457a5 100644 --- a/responder.go +++ b/responder.go @@ -20,13 +20,15 @@ type Encoder interface { } // Package-level variables for encoding the supported formats. They are set to -// our default implementations. By setting render.Encode{JSON,XML,Form} you can -// customize Encoding (e.g. you might want to configure the JSON-encoder) -// TODO document type constraints +// our default implementations. By setting +// render.Encode{JSON,XML,Data,PlainText} you can customize Encoding (e.g. you +// might want to configure the JSON-encoder) var ( EncoderJSON Encoder = EncodeJSON{} EncoderXML Encoder = EncodeXML{} + // EncoderData.Decode(w, req, v): v must be []byte EncoderData Encoder = EncodeData{} + // EncoderPlainText.Decode(w, req, v): v must be string EncoderPlainText Encoder = EncodePlainText{} ) @@ -83,6 +85,7 @@ type EncodePlainText struct{} // PlainText writes a string to the response, setting the Content-Type as // text/plain. +// vi has to be string func (EncodePlainText) Encode(w http.ResponseWriter, r *http.Request, vi interface{}) error { v, ok := vi.(string) if !ok { @@ -96,11 +99,23 @@ func (EncodePlainText) Encode(w http.ResponseWriter, r *http.Request, vi interfa return nil } -// TODO backwards compatible +// PlainText writes a string to the response, setting the Content-Type as +// text/plain. +// +// Deprecated: EncoderPlainText.Encode() should be used. +func PlainText(w http.ResponseWriter, r *http.Request, v string) { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + if status, ok := r.Context().Value(StatusCtxKey).(int); ok { + w.WriteHeader(status) + } + w.Write([]byte(v)) //nolint:errcheck +} + type EncodeData struct{} // Data writes raw bytes to the response, setting the Content-Type as // application/octet-stream. +// vi has to be []byte func (EncodeData) Encode(w http.ResponseWriter, r *http.Request, vi interface{}) error { v, ok := vi.([]byte) if !ok { @@ -114,10 +129,22 @@ func (EncodeData) Encode(w http.ResponseWriter, r *http.Request, vi interface{}) return nil } -// TODO backwards compatible +// Data writes raw bytes to the response, setting the Content-Type as +// application/octet-stream. +// +// Deprecated: EncoderXML.Encode() should be used. +func Data(w http.ResponseWriter, r *http.Request, v []byte) { + w.Header().Set("Content-Type", "application/octet-stream") + if status, ok := r.Context().Value(StatusCtxKey).(int); ok { + w.WriteHeader(status) + } + w.Write(v) //nolint:errcheck +} + type EncodeHTML struct{} // HTML writes a string to the response, setting the Content-Type as text/html. +// vi has to be a string. func (EncodeHTML) Encode(w http.ResponseWriter, r *http.Request, vi interface{}) error { v, ok := vi.(string) if !ok { @@ -131,6 +158,17 @@ func (EncodeHTML) Encode(w http.ResponseWriter, r *http.Request, vi interface{}) return nil } +// HTML writes a string to the response, setting the Content-Type as text/html. +// +// Deprecated: EncoderHTML.Encode() should be used. +func HTML(w http.ResponseWriter, r *http.Request, v string) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + if status, ok := r.Context().Value(StatusCtxKey).(int); ok { + w.WriteHeader(status) + } + w.Write([]byte(v)) //nolint:errcheck +} + type EncodeJSON struct{} // JSON marshals 'v' to JSON, automatically escaping HTML and setting the @@ -152,6 +190,27 @@ func (EncodeJSON) Encode(w http.ResponseWriter, r *http.Request, v interface{}) return nil } + +// JSON marshals 'v' to JSON, automatically escaping HTML and setting the +// Content-Type as application/json. +// +// Deprecated: EncoderJSON.Encode() should be used. +func JSON(w http.ResponseWriter, r *http.Request, v interface{}) { + buf := &bytes.Buffer{} + enc := json.NewEncoder(buf) + enc.SetEscapeHTML(true) + if err := enc.Encode(v); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + if status, ok := r.Context().Value(StatusCtxKey).(int); ok { + w.WriteHeader(status) + } + w.Write(buf.Bytes()) //nolint:errcheck +} + type EncodeXML struct{} // XML marshals 'v' to XML, setting the Content-Type as application/xml. It @@ -183,6 +242,36 @@ func (EncodeXML) Encode(w http.ResponseWriter, r *http.Request, v interface{}) e return nil } +// XML marshals 'v' to XML, setting the Content-Type as application/xml. It +// will automatically prepend a generic XML header (see encoding/xml.Header) if +// one is not found in the first 100 bytes of 'v'. +// +// Deprecated: EncoderXML.Encode() should be used. +func XML(w http.ResponseWriter, r *http.Request, v interface{}) { + b, err := xml.Marshal(v) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/xml; charset=utf-8") + if status, ok := r.Context().Value(StatusCtxKey).(int); ok { + w.WriteHeader(status) + } + + // Try to find 100 { + findHeaderUntil = 100 + } + if !bytes.Contains(b[:findHeaderUntil], []byte(" Date: Sat, 22 Apr 2023 18:56:06 +0200 Subject: [PATCH 3/3] fix documentation --- decoder.go | 7 ++++--- responder.go | 11 ++++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/decoder.go b/decoder.go index 2891794..c784744 100644 --- a/decoder.go +++ b/decoder.go @@ -14,6 +14,7 @@ import ( // Decoder is a generic interface to decode arbitrary data from a reader `r` to // a value `v`. type Decoder interface { + // Decodes a given reader into an interface Decode(r io.Reader, req *http.Request, v interface{}) error } @@ -55,7 +56,7 @@ func DefaultDecoder(r *http.Request, v interface{}) error { type DecodeJSONInter struct{} -// DecodeJSON decodes a given reader into an interface using the json decoder. +// Decodes a given reader into an interface using the json decoder. func (DecodeJSONInter) Decode(r io.Reader, req *http.Request, v interface{}) error { defer io.Copy(ioutil.Discard, r) //nolint:errcheck return json.NewDecoder(r).Decode(v) @@ -71,7 +72,7 @@ func DecodeJSON(r io.Reader, v interface{}) error { type DecodeXMLInter struct{} -// DecodeXML decodes a given reader into an interface using the xml decoder. +// Decodes a given reader into an interface using the xml decoder. func (DecodeXMLInter) Decode(r io.Reader, req *http.Request, v interface{}) error { defer io.Copy(ioutil.Discard, r) //nolint:errcheck return xml.NewDecoder(r).Decode(v) @@ -87,7 +88,7 @@ func DecodeXML(r io.Reader, v interface{}) error { type DecodeFormInter struct{} -// DecodeForm decodes a given reader into an interface using the form decoder. +// Decodes a given reader into an interface using the form decoder. func (DecodeFormInter) Decode(r io.Reader, req *http.Request, v interface{}) error { decoder := form.NewDecoder(r) //nolint:errcheck return decoder.Decode(v) diff --git a/responder.go b/responder.go index 17457a5..ed56ac8 100644 --- a/responder.go +++ b/responder.go @@ -16,6 +16,7 @@ var ErrInvalidType error = errors.New("Invalid Type passed") // Encoder is a generic interface to encode arbitrary data from value `v` to a // reader `r` type Encoder interface { + // Marshals 'v' to 'w' Encode(w http.ResponseWriter, req *http.Request, v interface{}) error } @@ -83,7 +84,7 @@ func DefaultResponder(w http.ResponseWriter, r *http.Request, v interface{}) { type EncodePlainText struct{} -// PlainText writes a string to the response, setting the Content-Type as +// Writes a string to the response, setting the Content-Type as // text/plain. // vi has to be string func (EncodePlainText) Encode(w http.ResponseWriter, r *http.Request, vi interface{}) error { @@ -113,7 +114,7 @@ func PlainText(w http.ResponseWriter, r *http.Request, v string) { type EncodeData struct{} -// Data writes raw bytes to the response, setting the Content-Type as +// Writes raw bytes to the response, setting the Content-Type as // application/octet-stream. // vi has to be []byte func (EncodeData) Encode(w http.ResponseWriter, r *http.Request, vi interface{}) error { @@ -143,7 +144,7 @@ func Data(w http.ResponseWriter, r *http.Request, v []byte) { type EncodeHTML struct{} -// HTML writes a string to the response, setting the Content-Type as text/html. +// Writes a string to the response, setting the Content-Type as text/html. // vi has to be a string. func (EncodeHTML) Encode(w http.ResponseWriter, r *http.Request, vi interface{}) error { v, ok := vi.(string) @@ -171,7 +172,7 @@ func HTML(w http.ResponseWriter, r *http.Request, v string) { type EncodeJSON struct{} -// JSON marshals 'v' to JSON, automatically escaping HTML and setting the +// Marshals 'v' to JSON, automatically escaping HTML and setting the // Content-Type as application/json. func (EncodeJSON) Encode(w http.ResponseWriter, r *http.Request, v interface{}) error { buf := &bytes.Buffer{} @@ -213,7 +214,7 @@ func JSON(w http.ResponseWriter, r *http.Request, v interface{}) { type EncodeXML struct{} -// XML marshals 'v' to XML, setting the Content-Type as application/xml. It +// Marshals 'v' to XML, setting the Content-Type as application/xml. It // will automatically prepend a generic XML header (see encoding/xml.Header) if // one is not found in the first 100 bytes of 'v'. func (EncodeXML) Encode(w http.ResponseWriter, r *http.Request, v interface{}) error {