diff options
author | wxiaoguang <wxiaoguang@gmail.com> | 2023-04-07 22:39:08 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-04-07 09:39:08 -0500 |
commit | 36c0840cf1696912b1872cfa4ebc4b241dabd693 (patch) | |
tree | c3951e2bb0f1723043bd7fdcec0ab50c19d6ef3e /modules | |
parent | 5b89670a318e52e271f65d96bfe1116d85d20988 (diff) | |
download | gitea-36c0840cf1696912b1872cfa4ebc4b241dabd693.tar.gz gitea-36c0840cf1696912b1872cfa4ebc4b241dabd693.zip |
Merge template functions "dict/Dict/mergeinto" (#23932)
One of the steps in #23328
Before there were 3 different but similar functions: dict/Dict/mergeinto
The code was just copied & pasted, no test.
This PR defines a new stable `dict` function, it covers all the 3 old
functions behaviors, only +160 -171
Future developers do not need to think about or guess the different dict
functions, just use one: `dict`
Why use `dict` but not `Dict`? Because there are far more `dict` than
`Dict` in code already ......
Diffstat (limited to 'modules')
-rw-r--r-- | modules/templates/helper.go | 92 | ||||
-rw-r--r-- | modules/templates/util.go | 47 | ||||
-rw-r--r-- | modules/templates/util_test.go | 43 | ||||
-rw-r--r-- | modules/util/util.go | 24 |
4 files changed, 95 insertions, 111 deletions
diff --git a/modules/templates/helper.go b/modules/templates/helper.go index 56be050481..407b2c64f5 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -8,7 +8,6 @@ import ( "bytes" "context" "encoding/hex" - "errors" "fmt" "html" "html/template" @@ -219,20 +218,6 @@ func NewFuncMap() []template.FuncMap { "DisableImportLocal": func() bool { return !setting.ImportLocalPaths }, - "Dict": func(values ...interface{}) (map[string]interface{}, error) { - if len(values)%2 != 0 { - return nil, errors.New("invalid dict call") - } - dict := make(map[string]interface{}, len(values)/2) - for i := 0; i < len(values); i += 2 { - key, ok := values[i].(string) - if !ok { - return nil, errors.New("dict keys must be strings") - } - dict[key] = values[i+1] - } - return dict, nil - }, "Printf": fmt.Sprintf, "Escape": Escape, "Sec2Time": util.SecToTime, @@ -242,35 +227,7 @@ func NewFuncMap() []template.FuncMap { "DefaultTheme": func() string { return setting.UI.DefaultTheme }, - // pass key-value pairs to a partial template which receives them as a dict - "dict": func(values ...interface{}) (map[string]interface{}, error) { - if len(values) == 0 { - return nil, errors.New("invalid dict call") - } - - dict := make(map[string]interface{}) - return util.MergeInto(dict, values...) - }, - /* like dict but merge key-value pairs into the first dict and return it */ - "mergeinto": func(root map[string]interface{}, values ...interface{}) (map[string]interface{}, error) { - if len(values) == 0 { - return nil, errors.New("invalid mergeinto call") - } - - dict := make(map[string]interface{}) - for key, value := range root { - dict[key] = value - } - - return util.MergeInto(dict, values...) - }, - "percentage": func(n int, values ...int) float32 { - sum := 0 - for i := 0; i < len(values); i++ { - sum += values[i] - } - return float32(n) * 100 / float32(sum) - }, + "dict": dict, "CommentMustAsDiff": gitdiff.CommentMustAsDiff, "MirrorRemoteAddress": mirrorRemoteAddress, "NotificationSettings": func() map[string]interface{} { @@ -413,52 +370,13 @@ func NewTextFuncMap() []texttmpl.FuncMap { }, "EllipsisString": base.EllipsisString, "URLJoin": util.URLJoin, - "Dict": func(values ...interface{}) (map[string]interface{}, error) { - if len(values)%2 != 0 { - return nil, errors.New("invalid dict call") - } - dict := make(map[string]interface{}, len(values)/2) - for i := 0; i < len(values); i += 2 { - key, ok := values[i].(string) - if !ok { - return nil, errors.New("dict keys must be strings") - } - dict[key] = values[i+1] - } - return dict, nil - }, - "Printf": fmt.Sprintf, - "Escape": Escape, - "Sec2Time": util.SecToTime, + "Printf": fmt.Sprintf, + "Escape": Escape, + "Sec2Time": util.SecToTime, "ParseDeadline": func(deadline string) []string { return strings.Split(deadline, "|") }, - "dict": func(values ...interface{}) (map[string]interface{}, error) { - if len(values) == 0 { - return nil, errors.New("invalid dict call") - } - - dict := make(map[string]interface{}) - - for i := 0; i < len(values); i++ { - switch key := values[i].(type) { - case string: - i++ - if i == len(values) { - return nil, errors.New("specify the key for non array values") - } - dict[key] = values[i] - case map[string]interface{}: - m := values[i].(map[string]interface{}) - for i, v := range m { - dict[i] = v - } - default: - return nil, errors.New("dict values must be maps") - } - } - return dict, nil - }, + "dict": dict, "QueryEscape": url.QueryEscape, "Eval": Eval, }} diff --git a/modules/templates/util.go b/modules/templates/util.go new file mode 100644 index 0000000000..13f3a56808 --- /dev/null +++ b/modules/templates/util.go @@ -0,0 +1,47 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package templates + +import ( + "fmt" + "reflect" +) + +func dictMerge(base map[string]any, arg any) bool { + if arg == nil { + return true + } + rv := reflect.ValueOf(arg) + if rv.Kind() == reflect.Map { + for _, k := range rv.MapKeys() { + base[k.String()] = rv.MapIndex(k).Interface() + } + return true + } + return false +} + +// dict is a helper function for creating a map[string]any from a list of key-value pairs. +// If the key is dot ".", the value is merged into the base map, just like Golang template's dot syntax: dot means current +// The dot syntax is highly discouraged because it might cause unclear key conflicts. It's always good to use explicit keys. +func dict(args ...any) (map[string]any, error) { + if len(args)%2 != 0 { + return nil, fmt.Errorf("invalid dict constructor syntax: must have key-value pairs") + } + m := make(map[string]any, len(args)/2) + for i := 0; i < len(args); i += 2 { + key, ok := args[i].(string) + if !ok { + return nil, fmt.Errorf("invalid dict constructor syntax: unable to merge args[%d]", i) + } + if key == "." { + if ok = dictMerge(m, args[i+1]); !ok { + return nil, fmt.Errorf("invalid dict constructor syntax: dot arg[%d] must be followed by a dict", i) + } + } else { + m[key] = args[i+1] + } + } + return m, nil +} diff --git a/modules/templates/util_test.go b/modules/templates/util_test.go new file mode 100644 index 0000000000..dfa691c5e2 --- /dev/null +++ b/modules/templates/util_test.go @@ -0,0 +1,43 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package templates + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDict(t *testing.T) { + type M map[string]any + cases := []struct { + args []any + want map[string]any + }{ + {[]any{"a", 1, "b", 2}, M{"a": 1, "b": 2}}, + {[]any{".", M{"base": 1}, "b", 2}, M{"base": 1, "b": 2}}, + {[]any{"a", 1, ".", M{"extra": 2}}, M{"a": 1, "extra": 2}}, + {[]any{"a", 1, ".", map[string]int{"int": 2}}, M{"a": 1, "int": 2}}, + {[]any{".", nil, "b", 2}, M{"b": 2}}, + } + + for _, c := range cases { + got, err := dict(c.args...) + if assert.NoError(t, err) { + assert.EqualValues(t, c.want, got) + } + } + + bads := []struct { + args []any + }{ + {[]any{"a", 1, "b"}}, + {[]any{1}}, + {[]any{struct{}{}}}, + } + for _, c := range bads { + _, err := dict(c.args...) + assert.Error(t, err) + } +} diff --git a/modules/util/util.go b/modules/util/util.go index 782b905bec..148817bd47 100644 --- a/modules/util/util.go +++ b/modules/util/util.go @@ -6,7 +6,6 @@ package util import ( "bytes" "crypto/rand" - "errors" "fmt" "math/big" "strconv" @@ -117,29 +116,6 @@ func NormalizeEOL(input []byte) []byte { return tmp[:pos] } -// MergeInto merges pairs of values into a "dict" -func MergeInto(dict map[string]interface{}, values ...interface{}) (map[string]interface{}, error) { - for i := 0; i < len(values); i++ { - switch key := values[i].(type) { - case string: - i++ - if i == len(values) { - return nil, errors.New("specify the key for non array values") - } - dict[key] = values[i] - case map[string]interface{}: - m := values[i].(map[string]interface{}) - for i, v := range m { - dict[i] = v - } - default: - return nil, errors.New("dict values must be maps") - } - } - - return dict, nil -} - // CryptoRandomInt returns a crypto random integer between 0 and limit, inclusive func CryptoRandomInt(limit int64) (int64, error) { rInt, err := rand.Int(rand.Reader, big.NewInt(limit)) |