From 0515fc94cba9a6488f3b2b746221cf3ccee16ac1 Mon Sep 17 00:00:00 2001 From: John Date: Tue, 14 Apr 2020 21:11:12 +0800 Subject: [PATCH] add MapMerge/MapMargeCopy functions for package gutil; improve template view feature for indefinite parameters --- frame/gmvc/view.go | 2 +- net/ghttp/ghttp_response_view.go | 29 +++---- os/gview/gview.go | 83 +++++++++---------- os/gview/gview_config.go | 29 +++++-- os/gview/gview_i18n.go | 6 +- os/gview/gview_parse.go | 81 ++++++------------- util/gutil/gutil_map.go | 29 +++++-- util/gutil/gutil_z_bench_test.go | 15 ++++ util/gutil/gutil_z_unit_map_test.go | 120 ++++++++++++++++++++++++++++ util/gutil/gutil_z_unit_test.go | 9 --- 10 files changed, 257 insertions(+), 146 deletions(-) create mode 100755 util/gutil/gutil_z_unit_map_test.go diff --git a/frame/gmvc/view.go b/frame/gmvc/view.go index b4e35bc71..73caebdea 100644 --- a/frame/gmvc/view.go +++ b/frame/gmvc/view.go @@ -99,7 +99,7 @@ func (view *View) BindFuncMap(funcMap gview.FuncMap) { // Display parses and writes the parsed template file content to http response. func (view *View) Display(file ...string) error { - name := "index.tpl" + name := view.view.GetDefaultFile() if len(file) > 0 { name = file[0] } diff --git a/net/ghttp/ghttp_response_view.go b/net/ghttp/ghttp_response_view.go index 2266e18ce..2bdde6473 100644 --- a/net/ghttp/ghttp_response_view.go +++ b/net/ghttp/ghttp_response_view.go @@ -11,6 +11,7 @@ import ( "github.com/gogf/gf/os/gcfg" "github.com/gogf/gf/os/gview" "github.com/gogf/gf/util/gmode" + "github.com/gogf/gf/util/gutil" ) // WriteTpl parses and responses given template file. @@ -74,27 +75,19 @@ func (r *Response) ParseTplContent(content string, params ...gview.Params) (stri // buildInVars merges build-in variables into and returns the new template variables. func (r *Response) buildInVars(params ...map[string]interface{}) map[string]interface{} { - var vars map[string]interface{} - if len(params) > 0 && params[0] != nil { - vars = params[0] - } else { - vars = make(map[string]interface{}) - } + m := gutil.MapMergeCopy(params...) // Retrieve custom template variables from request object. - if len(r.Request.viewParams) > 0 { - for k, v := range r.Request.viewParams { - vars[k] = v - } - } + gutil.MapMerge(m, r.Request.viewParams, map[string]interface{}{ + "Form": r.Request.GetFormMap(), + "Query": r.Request.GetQueryMap(), + "Request": r.Request.GetMap(), + "Cookie": r.Request.Cookie.Map(), + "Session": r.Request.Session.Map(), + }) // Note that it should assign no Config variable to template // if there's no configuration file. if c := gcfg.Instance(); c.Available() { - vars["Config"] = c.GetMap(".") + m["Config"] = c.GetMap(".") } - vars["Form"] = r.Request.GetFormMap() - vars["Query"] = r.Request.GetQueryMap() - vars["Request"] = r.Request.GetMap() - vars["Cookie"] = r.Request.Cookie.Map() - vars["Session"] = r.Request.Session.Map() - return vars + return m } diff --git a/os/gview/gview.go b/os/gview/gview.go index a8fa0832f..88d948c4b 100644 --- a/os/gview/gview.go +++ b/os/gview/gview.go @@ -12,7 +12,6 @@ package gview import ( "github.com/gogf/gf/container/gmap" - "github.com/gogf/gf/i18n/gi18n" "github.com/gogf/gf/internal/intlog" "github.com/gogf/gf" @@ -28,21 +27,12 @@ type View struct { data map[string]interface{} // Global template variables. funcMap map[string]interface{} // Global template function map. fileCacheMap *gmap.StrAnyMap // File cache map. - defaultFile string // Default template file for parsing. - i18nManager *gi18n.Manager // I18n manager for this view. - delimiters []string // Custom template delimiters. config Config // Extra configuration for the view. } -// Params is type for template params. -type Params = map[string]interface{} - -// FuncMap is type for custom template functions. -type FuncMap = map[string]interface{} - -const ( - // Default template file for parsing. - defaultParsingFile = "index.html" +type ( + Params = map[string]interface{} // Params is type for template params. + FuncMap = map[string]interface{} // FuncMap is type for custom template functions. ) var ( @@ -60,9 +50,9 @@ func checkAndInitDefaultView() { // ParseContent parses the template content directly using the default view object // and returns the parsed content. -func ParseContent(content string, params Params) (string, error) { +func ParseContent(content string, params ...Params) (string, error) { checkAndInitDefaultView() - return defaultViewObj.ParseContent(content, params) + return defaultViewObj.ParseContent(content, params...) } // New returns a new view object. @@ -73,9 +63,7 @@ func New(path ...string) *View { data: make(map[string]interface{}), funcMap: make(map[string]interface{}), fileCacheMap: gmap.NewStrAnyMap(true), - defaultFile: defaultParsingFile, - i18nManager: gi18n.Instance(), - delimiters: make([]string, 2), + config: DefaultConfig(), } if len(path) > 0 && len(path[0]) > 0 { if err := view.SetPath(path[0]); err != nil { @@ -118,35 +106,36 @@ func New(path ...string) *View { "version": gf.VERSION, } // default build-in functions. - view.BindFunc("eq", view.funcEq) - view.BindFunc("ne", view.funcNe) - view.BindFunc("lt", view.funcLt) - view.BindFunc("le", view.funcLe) - view.BindFunc("gt", view.funcGt) - view.BindFunc("ge", view.funcGe) - view.BindFunc("text", view.funcText) + view.BindFuncMap(FuncMap{ + "eq": view.funcEq, + "ne": view.funcNe, + "lt": view.funcLt, + "le": view.funcLe, + "gt": view.funcGt, + "ge": view.funcGe, + "text": view.funcText, + "html": view.funcHtmlEncode, + "htmlencode": view.funcHtmlEncode, + "htmldecode": view.funcHtmlDecode, + "encode": view.funcHtmlEncode, + "decode": view.funcHtmlDecode, + "url": view.funcUrlEncode, + "urlencode": view.funcUrlEncode, + "urldecode": view.funcUrlDecode, + "date": view.funcDate, + "substr": view.funcSubStr, + "strlimit": view.funcStrLimit, + "concat": view.funcConcat, + "replace": view.funcReplace, + "compare": view.funcCompare, + "hidestr": view.funcHideStr, + "highlight": view.funcHighlight, + "toupper": view.funcToUpper, + "tolower": view.funcToLower, + "nl2br": view.funcNl2Br, + "include": view.funcInclude, + "dump": view.funcDump, + }) - view.BindFunc("html", view.funcHtmlEncode) - view.BindFunc("htmlencode", view.funcHtmlEncode) - view.BindFunc("htmldecode", view.funcHtmlDecode) - view.BindFunc("encode", view.funcHtmlEncode) - view.BindFunc("decode", view.funcHtmlDecode) - - view.BindFunc("url", view.funcUrlEncode) - view.BindFunc("urlencode", view.funcUrlEncode) - view.BindFunc("urldecode", view.funcUrlDecode) - view.BindFunc("date", view.funcDate) - view.BindFunc("substr", view.funcSubStr) - view.BindFunc("strlimit", view.funcStrLimit) - view.BindFunc("concat", view.funcConcat) - view.BindFunc("replace", view.funcReplace) - view.BindFunc("compare", view.funcCompare) - view.BindFunc("hidestr", view.funcHideStr) - view.BindFunc("highlight", view.funcHighlight) - view.BindFunc("toupper", view.funcToUpper) - view.BindFunc("tolower", view.funcToLower) - view.BindFunc("nl2br", view.funcNl2Br) - view.BindFunc("include", view.funcInclude) - view.BindFunc("dump", view.funcDump) return view } diff --git a/os/gview/gview_config.go b/os/gview/gview_config.go index fc6045bc7..ad859dd84 100644 --- a/os/gview/gview_config.go +++ b/os/gview/gview_config.go @@ -22,10 +22,25 @@ import ( // Config is the configuration object for template engine. type Config struct { Paths []string // Searching array for path, NOT concurrent-safe for performance purpose. - Data map[string]interface{} // Global template variables. + Data map[string]interface{} // Global template variables including configuration. DefaultFile string // Default template file for parsing. Delimiters []string // Custom template delimiters. AutoEncode bool // Automatically encodes and provides safe html output, which is good for avoiding XSS. + I18nManager *gi18n.Manager // I18n manager for the view. +} + +const ( + // Default template file for parsing. + defaultParsingFile = "index.html" +) + +// DefaultConfig creates and returns a configuration object with default configurations. +func DefaultConfig() Config { + return Config{ + DefaultFile: defaultParsingFile, + I18nManager: gi18n.Instance(), + Delimiters: make([]string, 2), + } } // SetConfig sets the configuration for view. @@ -199,13 +214,17 @@ func (view *View) Assign(key string, value interface{}) { // SetDefaultFile sets default template file for parsing. func (view *View) SetDefaultFile(file string) { - view.defaultFile = file + view.config.DefaultFile = file +} + +// GetDefaultFile returns default template file for parsing. +func (view *View) GetDefaultFile() string { + return view.config.DefaultFile } // SetDelimiters sets customized delimiters for template parsing. func (view *View) SetDelimiters(left, right string) { - view.delimiters[0] = left - view.delimiters[1] = right + view.config.Delimiters = []string{left, right} } // SetAutoEncode enables/disables automatically html encoding feature. @@ -237,5 +256,5 @@ func (view *View) BindFuncMap(funcMap FuncMap) { // SetI18n binds i18n manager to current view engine. func (view *View) SetI18n(manager *gi18n.Manager) { - view.i18nManager = manager + view.config.I18nManager = manager } diff --git a/os/gview/gview_i18n.go b/os/gview/gview_i18n.go index 0aa653053..0e5fa1116 100644 --- a/os/gview/gview_i18n.go +++ b/os/gview/gview_i18n.go @@ -10,14 +10,14 @@ import "github.com/gogf/gf/util/gconv" // i18nTranslate translate the content with i18n feature. func (view *View) i18nTranslate(content string, params Params) string { - if view.i18nManager != nil { + if view.config.I18nManager != nil { if v, ok := params["I18nLanguage"]; ok { language := gconv.String(v) if language != "" { - return view.i18nManager.T(content, language) + return view.config.I18nManager.T(content, language) } } - return view.i18nManager.T(content) + return view.config.I18nManager.T(content) } return content } diff --git a/os/gview/gview_parse.go b/os/gview/gview_parse.go index 19f7b1c9b..4184aef9d 100644 --- a/os/gview/gview_parse.go +++ b/os/gview/gview_parse.go @@ -17,6 +17,7 @@ import ( "github.com/gogf/gf/os/gmlock" "github.com/gogf/gf/text/gstr" "github.com/gogf/gf/util/gconv" + "github.com/gogf/gf/util/gutil" htmltpl "html/template" "strconv" "strings" @@ -113,32 +114,9 @@ func (view *View) Parse(file string, params ...Params) (result string, err error // Note that the template variable assignment cannot change the value // of the existing or view.data because both variables are pointers. // It needs to merge the values of the two maps into a new map. - var variables map[string]interface{} - length := len(view.data) - if len(params) > 0 { - length += len(params[0]) - } - if length > 0 { - variables = make(map[string]interface{}, length) - } + variables := gutil.MapMergeCopy(params...) if len(view.data) > 0 { - if len(params) > 0 { - if variables == nil { - variables = make(map[string]interface{}) - } - for k, v := range params[0] { - variables[k] = v - } - for k, v := range view.data { - variables[k] = v - } - } else { - variables = view.data - } - } else { - if len(params) > 0 { - variables = params[0] - } + gutil.MapMerge(variables, view.data) } buffer := bytes.NewBuffer(nil) if view.config.AutoEncode { @@ -163,7 +141,7 @@ func (view *View) Parse(file string, params ...Params) (result string, err error // ParseDefault parses the default template file with params. func (view *View) ParseDefault(params ...Params) (result string, err error) { - return view.Parse(view.defaultFile, params...) + return view.Parse(view.config.DefaultFile, params...) } // ParseContent parses given template content with template variables @@ -174,12 +152,18 @@ func (view *View) ParseContent(content string, params ...Params) (string, error) return "", nil } err := (error)(nil) - key := fmt.Sprintf("%s_%v_%v", gCONTENT_TEMPLATE_NAME, view.delimiters, view.config.AutoEncode) + key := fmt.Sprintf("%s_%v_%v", gCONTENT_TEMPLATE_NAME, view.config.Delimiters, view.config.AutoEncode) tpl := templates.GetOrSetFuncLock(key, func() interface{} { if view.config.AutoEncode { - return htmltpl.New(gCONTENT_TEMPLATE_NAME).Delims(view.delimiters[0], view.delimiters[1]).Funcs(view.funcMap) + return htmltpl.New(gCONTENT_TEMPLATE_NAME).Delims( + view.config.Delimiters[0], + view.config.Delimiters[1], + ).Funcs(view.funcMap) } - return texttpl.New(gCONTENT_TEMPLATE_NAME).Delims(view.delimiters[0], view.delimiters[1]).Funcs(view.funcMap) + return texttpl.New(gCONTENT_TEMPLATE_NAME).Delims( + view.config.Delimiters[0], + view.config.Delimiters[1], + ).Funcs(view.funcMap) }) // Using memory lock to ensure concurrent safety for content parsing. hash := strconv.FormatUint(ghash.DJBHash64([]byte(content)), 10) @@ -196,32 +180,9 @@ func (view *View) ParseContent(content string, params ...Params) (string, error) // Note that the template variable assignment cannot change the value // of the existing or view.data because both variables are pointers. // It needs to merge the values of the two maps into a new map. - var variables map[string]interface{} - length := len(view.data) - if len(params) > 0 { - length += len(params[0]) - } - if length > 0 { - variables = make(map[string]interface{}, length) - } + variables := gutil.MapMergeCopy(params...) if len(view.data) > 0 { - if len(params) > 0 { - if variables == nil { - variables = make(map[string]interface{}) - } - for k, v := range params[0] { - variables[k] = v - } - for k, v := range view.data { - variables[k] = v - } - } else { - variables = view.data - } - } else { - if len(params) > 0 { - variables = params[0] - } + gutil.MapMerge(variables, view.data) } buffer := bytes.NewBuffer(nil) if view.config.AutoEncode { @@ -249,14 +210,20 @@ func (view *View) ParseContent(content string, params ...Params) (string, error) // if the template files under changes (recursively). func (view *View) getTemplate(filePath, folderPath, pattern string) (tpl interface{}, err error) { // Key for template cache. - key := fmt.Sprintf("%s_%v", filePath, view.delimiters) + key := fmt.Sprintf("%s_%v", filePath, view.config.Delimiters) result := templates.GetOrSetFuncLock(key, func() interface{} { // Do not use but the as the parameter for function New, // because when error occurs the will be printed out for error locating. if view.config.AutoEncode { - tpl = htmltpl.New(filePath).Delims(view.delimiters[0], view.delimiters[1]).Funcs(view.funcMap) + tpl = htmltpl.New(filePath).Delims( + view.config.Delimiters[0], + view.config.Delimiters[1], + ).Funcs(view.funcMap) } else { - tpl = texttpl.New(filePath).Delims(view.delimiters[0], view.delimiters[1]).Funcs(view.funcMap) + tpl = texttpl.New(filePath).Delims( + view.config.Delimiters[0], + view.config.Delimiters[1], + ).Funcs(view.funcMap) } // Firstly checking the resource manager. if !gres.IsEmpty() { diff --git a/util/gutil/gutil_map.go b/util/gutil/gutil_map.go index c787075ef..ff00b5cf8 100644 --- a/util/gutil/gutil_map.go +++ b/util/gutil/gutil_map.go @@ -8,12 +8,6 @@ package gutil import ( "github.com/gogf/gf/internal/utils" - "regexp" -) - -var ( - // replaceCharReg is the regular expression object for replacing chars in map keys. - replaceCharReg, _ = regexp.Compile(`[\-\.\_\s]+`) ) // MapCopy does a shallow copy from map to for most commonly used map type @@ -32,6 +26,29 @@ func MapContains(data map[string]interface{}, key string) (ok bool) { return } +// MapMerge merges all map from to map . +func MapMerge(dst map[string]interface{}, src ...map[string]interface{}) { + if dst == nil { + return + } + for _, m := range src { + for k, v := range m { + dst[k] = v + } + } +} + +// MapMergeCopy creates and returns a new map which merges all map from . +func MapMergeCopy(src ...map[string]interface{}) (copy map[string]interface{}) { + copy = make(map[string]interface{}) + for _, m := range src { + for k, v := range m { + copy[k] = v + } + } + return +} + // MapPossibleItemByKey tries to find the possible key-value pair for given key with or without // cases or chars '-'/'_'/'.'/' '. // diff --git a/util/gutil/gutil_z_bench_test.go b/util/gutil/gutil_z_bench_test.go index c68f38421..2276c021f 100644 --- a/util/gutil/gutil_z_bench_test.go +++ b/util/gutil/gutil_z_bench_test.go @@ -12,6 +12,15 @@ import ( "testing" ) +var ( + m1 = map[string]interface{}{ + "k1": "v1", + } + m2 = map[string]interface{}{ + "k2": "v2", + } +) + func Benchmark_TryCatch(b *testing.B) { for i := 0; i < b.N; i++ { TryCatch(func() { @@ -21,3 +30,9 @@ func Benchmark_TryCatch(b *testing.B) { }) } } + +func Benchmark_MapMergeCopy(b *testing.B) { + for i := 0; i < b.N; i++ { + MapMergeCopy(m1, m2) + } +} diff --git a/util/gutil/gutil_z_unit_map_test.go b/util/gutil/gutil_z_unit_map_test.go new file mode 100755 index 000000000..e71b032ce --- /dev/null +++ b/util/gutil/gutil_z_unit_map_test.go @@ -0,0 +1,120 @@ +// Copyright 2019 gf Author(https://github.com/gogf/gf). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package gutil_test + +import ( + "github.com/gogf/gf/frame/g" + "testing" + + "github.com/gogf/gf/test/gtest" + "github.com/gogf/gf/util/gutil" +) + +func Test_MapCopy(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + m1 := g.Map{ + "k1": "v1", + } + m2 := gutil.MapCopy(m1) + m2["k2"] = "v2" + + t.Assert(m1["k1"], "v1") + t.Assert(m1["k2"], nil) + t.Assert(m2["k1"], "v1") + t.Assert(m2["k2"], "v2") + }) +} + +func Test_MapContains(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + m1 := g.Map{ + "k1": "v1", + } + t.Assert(gutil.MapContains(m1, "k1"), true) + t.Assert(gutil.MapContains(m1, "K1"), false) + t.Assert(gutil.MapContains(m1, "k2"), false) + }) +} + +func Test_MapMerge(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + m1 := g.Map{ + "k1": "v1", + } + m2 := g.Map{ + "k2": "v2", + } + m3 := g.Map{ + "k3": "v3", + } + gutil.MapMerge(m1, m2, m3, nil) + t.Assert(m1["k1"], "v1") + t.Assert(m1["k2"], "v2") + t.Assert(m1["k3"], "v3") + t.Assert(m2["k1"], nil) + t.Assert(m3["k1"], nil) + }) +} + +func Test_MapMergeCopy(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + m1 := g.Map{ + "k1": "v1", + } + m2 := g.Map{ + "k2": "v2", + } + m3 := g.Map{ + "k3": "v3", + } + m := gutil.MapMergeCopy(m1, m2, m3, nil) + t.Assert(m["k1"], "v1") + t.Assert(m["k2"], "v2") + t.Assert(m["k3"], "v3") + t.Assert(m1["k1"], "v1") + t.Assert(m1["k2"], nil) + t.Assert(m2["k1"], nil) + t.Assert(m3["k1"], nil) + }) +} + +func Test_MapPossibleItemByKey(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + m := g.Map{ + "name": "guo", + "NickName": "john", + } + k, v := gutil.MapPossibleItemByKey(m, "NAME") + t.Assert(k, "name") + t.Assert(v, "guo") + + k, v = gutil.MapPossibleItemByKey(m, "nick name") + t.Assert(k, "NickName") + t.Assert(v, "john") + + k, v = gutil.MapPossibleItemByKey(m, "none") + t.Assert(k, "") + t.Assert(v, nil) + }) +} + +func Test_MapContainsPossibleKey(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + m := g.Map{ + "name": "guo", + "NickName": "john", + } + t.Assert(gutil.MapContainsPossibleKey(m, "name"), true) + t.Assert(gutil.MapContainsPossibleKey(m, "NAME"), true) + t.Assert(gutil.MapContainsPossibleKey(m, "nickname"), true) + t.Assert(gutil.MapContainsPossibleKey(m, "nick name"), true) + t.Assert(gutil.MapContainsPossibleKey(m, "nick_name"), true) + t.Assert(gutil.MapContainsPossibleKey(m, "nick-name"), true) + t.Assert(gutil.MapContainsPossibleKey(m, "nick.name"), true) + t.Assert(gutil.MapContainsPossibleKey(m, "none"), false) + }) +} diff --git a/util/gutil/gutil_z_unit_test.go b/util/gutil/gutil_z_unit_test.go index c9b4a0ec5..dbd6d0469 100755 --- a/util/gutil/gutil_z_unit_test.go +++ b/util/gutil/gutil_z_unit_test.go @@ -19,18 +19,9 @@ func Test_Dump(t *testing.T) { 100: 100, }) }) - - gtest.C(t, func(t *gtest.T) { - gutil.Dump(map[string]interface{}{"": func() {}}) - }) - - gtest.C(t, func(t *gtest.T) { - gutil.Dump([]byte("gutil Dump test")) - }) } func Test_TryCatch(t *testing.T) { - gtest.C(t, func(t *gtest.T) { gutil.TryCatch(func() { panic("gutil TryCatch test")