From 57aea4aa753c7cddee90aa810fc71ba4694d81a0 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Wed, 1 Mar 2017 15:04:42 +0200 Subject: [PATCH] Move the kataras/go-serializer into one iris' file. Start organising the kataras/go-*package which are relative to the Iris project. The kataras/go-*package will be exists and their features will be adapted to Iris every 2 months, as we say at readme, iris-relative packages should be tested for a long time before adapted to Iris. With this way we have stability, code readability(the developer can easly navigate to iris' code without need to move across different kataras/ projects). Former-commit-id: db291faaf59d4f53f14ce5800fde805f56c8b802 --- adaptors/httprouter/httprouter.go | 17 +- adaptors/view/adaptor.go | 6 +- context.go | 2 +- iris.go | 40 +-- policy.go | 10 +- serializer.go | 266 ++++++++++++++++++++ policy_render_test.go => serializer_test.go | 0 7 files changed, 287 insertions(+), 54 deletions(-) create mode 100644 serializer.go rename policy_render_test.go => serializer_test.go (100%) diff --git a/adaptors/httprouter/httprouter.go b/adaptors/httprouter/httprouter.go index db8e8b59..ee929992 100644 --- a/adaptors/httprouter/httprouter.go +++ b/adaptors/httprouter/httprouter.go @@ -522,14 +522,17 @@ func New() iris.Policies { RouterReversionPolicy: iris.RouterReversionPolicy{ // path normalization done on iris' side StaticPath: func(path string) string { - i := strings.IndexByte(path, parameterStartByte) - x := strings.IndexByte(path, matchEverythingByte) - if i > -1 { - return path[0:i] - } - if x > -1 { - return path[0:x] + v := strings.IndexByte(path, matchEverythingByte) + if i > -1 || v > -1 { + if i < v { + return path[0:i] + } + // we can't return path[0:0] + if v > 0 { + return path[0:v] + } + } return path diff --git a/adaptors/view/adaptor.go b/adaptors/view/adaptor.go index b0293c89..bb435d29 100644 --- a/adaptors/view/adaptor.go +++ b/adaptors/view/adaptor.go @@ -125,13 +125,13 @@ func (h *Adaptor) Adapt(frame *iris.Policies) { // adapt the build event to the main policies evt.Adapt(frame) - r := iris.RenderPolicy(func(out io.Writer, file string, tmplContext interface{}, options ...map[string]interface{}) (error, bool) { + r := iris.RenderPolicy(func(out io.Writer, file string, tmplContext interface{}, options ...map[string]interface{}) (bool, error) { // template mux covers that but maybe we have more than one RenderPolicy // and each of them carries a different mux on the new design. if strings.Contains(file, h.extension) { - return mux.ExecuteWriter(out, file, tmplContext, options...), true + return true, mux.ExecuteWriter(out, file, tmplContext, options...) } - return nil, false + return false, nil }) r.Adapt(frame) diff --git a/context.go b/context.go index ffad1caf..c49bedcb 100644 --- a/context.go +++ b/context.go @@ -1034,7 +1034,7 @@ func (ctx *Context) XML(status int, v interface{}) error { // MarkdownString parses the (dynamic) markdown string and returns the converted html string func (ctx *Context) MarkdownString(markdownText string) string { out := &bytes.Buffer{} - _, ok := ctx.framework.policies.RenderPolicy(out, contentMarkdown, markdownText) + ok, _ := ctx.framework.policies.RenderPolicy(out, contentMarkdown, markdownText) if ok { return out.String() } diff --git a/iris.go b/iris.go index 2e8358c7..e4dde7ec 100644 --- a/iris.go +++ b/iris.go @@ -28,7 +28,6 @@ import ( "github.com/geekypanda/httpcache" "github.com/kataras/go-errors" "github.com/kataras/go-fs" - "github.com/kataras/go-serializer" ) const ( @@ -260,42 +259,7 @@ func New(setters ...OptionSetter) *Framework { // | Adapt one RenderPolicy which is responsible | // | for json,jsonp,xml and markdown rendering | // +------------------------------------------------------------+ - - // prepare the serializers, - // serializer content-types(json,jsonp,xml,markdown) the defaults are setted: - serializers := serializer.Serializers{} - serializer.RegisterDefaults(serializers) - - // - // notes for me: Why not at the build state? in order to be overridable and not only them, - // these are easy to be overridden by external adaptors too, no matter the order, - // this is why the RenderPolicy last registration executing first and the first last. - // - - // Adapt the RenderPolicy on the Build in order to be the last - // render policy, so the users can adapt their own before the default(= to override json,xml,jsonp renderer). - // - // Notes: the Renderer of the view system is managed by the - // adaptors because they are optional. - // If templates are binded to the RenderPolicy then - // If a key contains a dot('.') then is a template file - // otherwise try to find a serializer, if contains error then we return false and the error - // in order the renderer to continue to search for any other custom registerer RenderPolicy - // if no error then check if it has written anything, if yes write the content - // to the writer(which is the context.ResponseWriter or the gzip version of it) - // if no error but nothing written then we return false and the error - s.Adapt(RenderPolicy(func(out io.Writer, name string, bind interface{}, options ...map[string]interface{}) (error, bool) { - b, err := serializers.Serialize(name, bind, options...) - if err != nil { - return err, false // errors should be wrapped - } - if len(b) > 0 { - _, err = out.Write(b) - return err, true - } - // continue to the next if any or notice there is no available renderer for that name - return nil, false - })) + s.Adapt(restRenderPolicy) } { // +------------------------------------------------------------+ @@ -1061,7 +1025,7 @@ type RenderOptions map[string]interface{} // // It can also render json,xml,jsonp and markdown by-default before or after .Build too. func (s *Framework) Render(w io.Writer, name string, bind interface{}, options ...map[string]interface{}) error { - err, ok := s.policies.RenderPolicy(w, name, bind, options...) + ok, err := s.policies.RenderPolicy(w, name, bind, options...) if !ok { // ok is false ONLY WHEN there is no registered render policy // that is responsible for that 'name` (if contains dot '.' it's for templates). diff --git a/policy.go b/policy.go index 07dfca06..08a0c367 100644 --- a/policy.go +++ b/policy.go @@ -379,7 +379,7 @@ func (r RouterWrapperPolicy) Adapt(frame *Policies) { // - the first registered is executing last. // So a custom adaptor that the community can create and share with each other // can override the existing one with just a simple registration. -type RenderPolicy func(out io.Writer, name string, bind interface{}, options ...map[string]interface{}) (error, bool) +type RenderPolicy func(out io.Writer, name string, bind interface{}, options ...map[string]interface{}) (bool, error) // Adapt adaps a RenderPolicy object to the main *Policies. func (r RenderPolicy) Adapt(frame *Policies) { @@ -388,14 +388,14 @@ func (r RenderPolicy) Adapt(frame *Policies) { prevRenderer := frame.RenderPolicy if prevRenderer != nil { nextRenderer := r - renderer = func(out io.Writer, name string, binding interface{}, options ...map[string]interface{}) (error, bool) { + renderer = func(out io.Writer, name string, binding interface{}, options ...map[string]interface{}) (bool, error) { // Remember: RenderPolicy works in the opossite order of declaration, // the last registered is trying to be executed first, // the first registered is executing last. - err, ok := nextRenderer(out, name, binding, options...) + ok, err := nextRenderer(out, name, binding, options...) if !ok { - prevErr, prevOk := prevRenderer(out, name, binding, options...) + prevOk, prevErr := prevRenderer(out, name, binding, options...) if err != nil { if prevErr != nil { err = errors.New(prevErr.Error()).Append(err.Error()) @@ -407,7 +407,7 @@ func (r RenderPolicy) Adapt(frame *Policies) { } // this renderer is responsible for this name // but it has an error, so don't continue to the next - return err, ok + return ok, err } } diff --git a/serializer.go b/serializer.go new file mode 100644 index 00000000..3e4bd2b3 --- /dev/null +++ b/serializer.go @@ -0,0 +1,266 @@ +package iris + +import ( + "bytes" + "encoding/json" + "encoding/xml" + "io" + + "github.com/microcosm-cc/bluemonday" + "github.com/russross/blackfriday" + "github.com/valyala/bytebufferpool" +) + +// these are the default render policies for basic REST-type render for content types: +// - application/javascript (json) +// - text/javascript (jsonp) +// - text/xml (xml) +// - custom internal text/markdown -> text/html (markdown) + +// the fastest buffer pool is maden by valyala, we use that because Iris should be fast at every step. +var buffer bytebufferpool.Pool + +// some options-helpers here +func tryParseStringOption(options map[string]interface{}, key string, defValue string) string { + if tryVal := options[key]; tryVal != nil { + if val, ok := tryVal.(string); ok { + return val + } + } + return defValue +} + +func tryParseBoolOption(options map[string]interface{}, key string, defValue bool) bool { + if tryVal := options[key]; tryVal != nil { + if val, ok := tryVal.(bool); ok { + return val + } + } + return defValue +} + +func tryParseByteSliceOption(options map[string]interface{}, key string, defValue []byte) []byte { + if tryVal := options[key]; tryVal != nil { + if val, ok := tryVal.([]byte); ok { + return val + } + } + return defValue +} + +// +------------------------------------------------------------+ +// | JSON | +// +------------------------------------------------------------+ + +var ( + newLineB = []byte("\n") + // the html codes for unescaping + ltHex = []byte("\\u003c") + lt = []byte("<") + + gtHex = []byte("\\u003e") + gt = []byte(">") + + andHex = []byte("\\u0026") + and = []byte("&") +) + +// Let's no use map here and do a func which will do simple and fast if statements. +// var serializers = map[string]func(interface{}, ...map[string]interface{}) ([]byte, error){ +// contentJSON: serializeJSON, +// contentJSONP: serializeJSONP, +// contentXML: serializeXML, +// contentMarkdown: serializeMarkdown, +// } + +var restRenderPolicy = RenderPolicy(func(out io.Writer, name string, val interface{}, options ...map[string]interface{}) (bool, error) { + var ( + b []byte + err error + ) + + if name == contentJSON { + b, err = serializeJSON(val, options...) + } else if name == contentJSONP { + b, err = serializeJSONP(val, options...) + } else if name == contentXML { + b, err = serializeXML(val, options...) + } else if name == contentMarkdown { + b, err = serializeMarkdown(val, options...) + } + + if err != nil { + return false, err // errors are wrapped + } + if len(b) > 0 { + _, err = out.Write(b) + return true, err + } + + // continue to the next if any or notice there is no available renderer for that name + return false, nil +}) + +// serializeJSON accepts the 'object' value and converts it to bytes in order to be json 'renderable' +func serializeJSON(val interface{}, options ...map[string]interface{}) ([]byte, error) { + // parse the options + var ( + indent bool + unEscapeHTML bool + streamingJSON bool + prefix []byte + ) + + if options != nil && len(options) > 0 { + opt := options[0] + indent = tryParseBoolOption(opt, "indent", false) + unEscapeHTML = tryParseBoolOption(opt, "unEscapeHTML", false) + streamingJSON = tryParseBoolOption(opt, "streamingJSON", false) + prefix = tryParseByteSliceOption(opt, "prefix", []byte("")) + } + + // serialize the 'object' + if streamingJSON { + w := buffer.Get() + if len(prefix) > 0 { + w.Write(prefix) + } + err := json.NewEncoder(w).Encode(val) + result := w.Bytes() + buffer.Put(w) + return result, err + } + + var result []byte + var err error + + if indent { + result, err = json.MarshalIndent(val, "", " ") + result = append(result, newLineB...) + } else { + result, err = json.Marshal(val) + } + if err != nil { + return nil, err + } + + if unEscapeHTML { + result = bytes.Replace(result, ltHex, lt, -1) + result = bytes.Replace(result, gtHex, gt, -1) + result = bytes.Replace(result, andHex, and, -1) + } + if len(prefix) > 0 { + result = append(prefix, result...) + } + return result, nil +} + +// +------------------------------------------------------------+ +// | JSONP | +// +------------------------------------------------------------+ + +var ( + finishCallbackB = []byte(");") +) + +// serializeJSONP accepts the 'object' value and converts it to bytes in order to be jsonp 'renderable' +func serializeJSONP(val interface{}, options ...map[string]interface{}) ([]byte, error) { + // parse the options + var ( + indent bool + callback string + ) + if options != nil && len(options) > 0 { + opt := options[0] + indent = tryParseBoolOption(opt, "indent", false) + callback = tryParseStringOption(opt, "callback", "") + } + + var result []byte + var err error + + if indent { + result, err = json.MarshalIndent(val, "", " ") + } else { + result, err = json.Marshal(val) + } + + if err != nil { + return nil, err + } + + if callback != "" { + result = append([]byte(callback+"("), result...) + result = append(result, finishCallbackB...) + } + + if indent { + result = append(result, newLineB...) + } + return result, nil +} + +// +------------------------------------------------------------+ +// | XML | +// +------------------------------------------------------------+ + +// serializeXML accepts the 'object' value and converts it to bytes in order to be xml 'renderable' +func serializeXML(val interface{}, options ...map[string]interface{}) ([]byte, error) { + // parse the options + var ( + indent bool + prefix []byte + ) + if options != nil && len(options) > 0 { + opt := options[0] + indent = tryParseBoolOption(opt, "indent", false) + prefix = tryParseByteSliceOption(opt, "prefix", []byte("")) + } + + var result []byte + var err error + + if indent { + result, err = xml.MarshalIndent(val, "", " ") + result = append(result, '\n') + } else { + result, err = xml.Marshal(val) + } + if err != nil { + return nil, err + } + if len(prefix) > 0 { + result = append(prefix, result...) + } + return result, nil +} + +// +------------------------------------------------------------+ +// | MARKDOWN | +// +------------------------------------------------------------+ + +// serializeMarkdown accepts the 'object' value and converts it to bytes in order to be markdown(text/html) 'renderable' +func serializeMarkdown(val interface{}, options ...map[string]interface{}) ([]byte, error) { + + // parse the options + var ( + sanitize bool + ) + if options != nil && len(options) > 0 { + opt := options[0] + sanitize = tryParseBoolOption(opt, "sanitize", false) + } + + var b []byte + if s, isString := val.(string); isString { + b = []byte(s) + } else { + b = val.([]byte) + } + buf := blackfriday.MarkdownCommon(b) + if sanitize { + buf = bluemonday.UGCPolicy().SanitizeBytes(buf) + } + + return buf, nil +} diff --git a/policy_render_test.go b/serializer_test.go similarity index 100% rename from policy_render_test.go rename to serializer_test.go