diff --git a/.github/ISSUE_TEMPLATE.MD b/.github/ISSUE_TEMPLATE.MD index 364044d16..540e46b4b 100644 --- a/.github/ISSUE_TEMPLATE.MD +++ b/.github/ISSUE_TEMPLATE.MD @@ -1,12 +1,11 @@ - - - + + + ### 1. What version of `Go` and system type/arch are you using? - @@ -23,7 +21,6 @@ What expect to see is like: `go 1.12, linux/amd64` ### 4. What did you do? - \n") return buffer.String() @@ -224,7 +229,42 @@ func (view *View) buildInFuncNl2Br(str interface{}) string { // buildInFuncJson implements build-in template function: json , // which encodes and returns `value` as JSON string. func (view *View) buildInFuncJson(value interface{}) (string, error) { - b, err := json.Marshal(value) + b, err := gjson.Marshal(value) + return string(b), err +} + +// buildInFuncXml implements build-in template function: xml , +// which encodes and returns `value` as XML string. +func (view *View) buildInFuncXml(value interface{}, rootTag ...string) (string, error) { + b, err := gjson.New(value).ToXml(rootTag...) + return string(b), err +} + +// buildInFuncXml implements build-in template function: ini , +// which encodes and returns `value` as XML string. +func (view *View) buildInFuncIni(value interface{}) (string, error) { + b, err := gjson.New(value).ToIni() + return string(b), err +} + +// buildInFuncYaml implements build-in template function: yaml , +// which encodes and returns `value` as YAML string. +func (view *View) buildInFuncYaml(value interface{}) (string, error) { + b, err := gjson.New(value).ToYaml() + return string(b), err +} + +// buildInFuncYamlIndent implements build-in template function: yamli , +// which encodes and returns `value` as YAML string with custom indent string. +func (view *View) buildInFuncYamlIndent(value, indent interface{}) (string, error) { + b, err := gjson.New(value).ToYamlIndent(gconv.String(indent)) + return string(b), err +} + +// buildInFuncToml implements build-in template function: toml , +// which encodes and returns `value` as TOML string. +func (view *View) buildInFuncToml(value interface{}) (string, error) { + b, err := gjson.New(value).ToToml() return string(b), err } diff --git a/os/gview/gview_parse.go b/os/gview/gview_parse.go index 418bb3fdf..44ccbffca 100644 --- a/os/gview/gview_parse.go +++ b/os/gview/gview_parse.go @@ -59,9 +59,64 @@ var ( // Parse parses given template file `file` with given template variables `params` // and returns the parsed template content. func (view *View) Parse(ctx context.Context, file string, params ...Params) (result string, err error) { - var tpl interface{} - // It caches the file, folder and its content to enhance performance. - r := view.fileCacheMap.GetOrSetFuncLock(file, func() interface{} { + var usedParams Params + if len(params) > 0 { + usedParams = params[0] + } + return view.ParseOption(ctx, Option{ + File: file, + Content: "", + Orphan: false, + Params: usedParams, + }) +} + +// ParseDefault parses the default template file with params. +func (view *View) ParseDefault(ctx context.Context, params ...Params) (result string, err error) { + var usedParams Params + if len(params) > 0 { + usedParams = params[0] + } + return view.ParseOption(ctx, Option{ + File: view.config.DefaultFile, + Content: "", + Orphan: false, + Params: usedParams, + }) +} + +// ParseContent parses given template content `content` with template variables `params` +// and returns the parsed content in []byte. +func (view *View) ParseContent(ctx context.Context, content string, params ...Params) (string, error) { + var usedParams Params + if len(params) > 0 { + usedParams = params[0] + } + return view.ParseOption(ctx, Option{ + Content: content, + Orphan: false, + Params: usedParams, + }) +} + +// Option for template parsing. +type Option struct { + File string // Template file path in absolute or relative to searching paths. + Content string // Template content, it ignores `File` if `Content` is given. + Orphan bool // If true, the `File` is considered as a single file parsing without files recursively parsing from its folder. + Params Params // Template parameters map. +} + +// ParseOption implements template parsing using Option. +func (view *View) ParseOption(ctx context.Context, option Option) (result string, err error) { + if option.Content != "" { + return view.doParseContent(ctx, option.Content, option.Params) + } + if option.File == "" { + return "", gerror.New(`template file cannot be empty`) + } + // It caches the file, folder and content to enhance performance. + r := view.fileCacheMap.GetOrSetFuncLock(option.File, func() interface{} { var ( path string folder string @@ -69,7 +124,7 @@ func (view *View) Parse(ctx context.Context, file string, params ...Params) (res resource *gres.File ) // Searching the absolute file path for `file`. - path, folder, resource, err = view.searchFile(ctx, file) + path, folder, resource, err = view.searchFile(ctx, option.File) if err != nil { return nil } @@ -80,13 +135,13 @@ func (view *View) Parse(ctx context.Context, file string, params ...Params) (res } // Monitor template files changes using fsnotify asynchronously. if resource == nil { - if _, err := gfsnotify.AddOnce("gview.Parse:"+folder, folder, func(event *gfsnotify.Event) { + if _, err = gfsnotify.AddOnce("gview.Parse:"+folder, folder, func(event *gfsnotify.Event) { // CLEAR THEM ALL. view.fileCacheMap.Clear() templates.Clear() gfsnotify.Exit() }); err != nil { - intlog.Error(ctx, err) + intlog.Errorf(ctx, `%+v`, err) } } return &fileCacheItem{ @@ -103,7 +158,12 @@ func (view *View) Parse(ctx context.Context, file string, params ...Params) (res if item.content == "" { return "", nil } + // If it's Orphan option, it just parses the single file by ParseContent. + if option.Orphan { + return view.doParseContent(ctx, item.content, option.Params) + } // Get the template object instance for `folder`. + var tpl interface{} tpl, err = view.getTemplate(item.path, item.folder, fmt.Sprintf(`*%s`, gfile.Ext(item.path))) if err != nil { return "", err @@ -125,7 +185,7 @@ func (view *View) Parse(ctx context.Context, file string, params ...Params) (res // Note that the template variable assignment cannot change the value // of the existing `params` or view.data because both variables are pointers. // It needs to merge the values of the two maps into a new map. - variables := gutil.MapMergeCopy(params...) + variables := gutil.MapMergeCopy(option.Params) if len(view.data) > 0 { gutil.MapMerge(variables, view.data) } @@ -137,11 +197,11 @@ func (view *View) Parse(ctx context.Context, file string, params ...Params) (res if err != nil { return "", err } - if err := newTpl.Execute(buffer, variables); err != nil { + if err = newTpl.Execute(buffer, variables); err != nil { return "", err } } else { - if err := tpl.(*texttpl.Template).Execute(buffer, variables); err != nil { + if err = tpl.(*texttpl.Template).Execute(buffer, variables); err != nil { return "", err } } @@ -152,14 +212,9 @@ func (view *View) Parse(ctx context.Context, file string, params ...Params) (res return result, nil } -// ParseDefault parses the default template file with params. -func (view *View) ParseDefault(ctx context.Context, params ...Params) (result string, err error) { - return view.Parse(ctx, view.config.DefaultFile, params...) -} - -// ParseContent parses given template content `content` with template variables `params` +// doParseContent parses given template content `content` with template variables `params` // and returns the parsed content in []byte. -func (view *View) ParseContent(ctx context.Context, content string, params ...Params) (string, error) { +func (view *View) doParseContent(ctx context.Context, content string, params Params) (string, error) { // It's not necessary continuing parsing if template content is empty. if content == "" { return "", nil @@ -181,7 +236,7 @@ func (view *View) ParseContent(ctx context.Context, content string, params ...Pa }) ) // Using memory lock to ensure concurrent safety for content parsing. - hash := strconv.FormatUint(ghash.DJBHash64([]byte(content)), 10) + hash := strconv.FormatUint(ghash.DJB64([]byte(content)), 10) gmlock.LockFunc("gview.ParseContent:"+hash, func() { if view.config.AutoEncode { tpl, err = tpl.(*htmltpl.Template).Parse(content) @@ -196,7 +251,7 @@ func (view *View) ParseContent(ctx context.Context, content string, params ...Pa // Note that the template variable assignment cannot change the value // of the existing `params` or view.data because both variables are pointers. // It needs to merge the values of the two maps into a new map. - variables := gutil.MapMergeCopy(params...) + variables := gutil.MapMergeCopy(params) if len(view.data) > 0 { gutil.MapMerge(variables, view.data) } @@ -272,10 +327,9 @@ func (view *View) getTemplate(filePath, folderPath, pattern string) (tpl interfa } } - // Secondly checking the file system. - var ( - files []string - ) + // Secondly checking the file system, + // and then automatically parsing all its sub-files recursively. + var files []string files, err = gfile.ScanDir(folderPath, pattern, true) if err != nil { return nil @@ -319,9 +373,7 @@ func (view *View) formatTemplateObjectCreatingError(filePath, tplName string, er // Note that, the returned `folder` is the template folder path, but not the folder of // the returned template file `path`. func (view *View) searchFile(ctx context.Context, file string) (path string, folder string, resource *gres.File, err error) { - var ( - tempPath string - ) + var tempPath string // Firstly checking the resource manager. if !gres.IsEmpty() { // Try folders. @@ -350,6 +402,13 @@ func (view *View) searchFile(ctx context.Context, file string) (path string, fol // Secondly checking the file system. if path == "" { + // Absolute path. + path = gfile.RealPath(file) + if path != "" { + folder = gfile.Dir(path) + return + } + // In search paths. view.searchPaths.RLockFunc(func(array []string) { for _, searchPath := range array { searchPath = gstr.TrimRight(searchPath, `\/`) @@ -359,7 +418,7 @@ func (view *View) searchFile(ctx context.Context, file string) (path string, fol `\/`, ) if path, _ = gspath.Search(searchPath, relativePath); path != "" { - folder = gfile.Dir(path) + folder = gfile.Join(searchPath, tryFolder) return } } diff --git a/os/gview/gview_z_unit_config_test.go b/os/gview/gview_z_unit_config_test.go index ea70d0a08..f138203c0 100644 --- a/os/gview/gview_z_unit_config_test.go +++ b/os/gview/gview_z_unit_config_test.go @@ -10,7 +10,6 @@ import ( "context" "testing" - "github.com/gogf/gf/v2/debug/gdebug" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gview" "github.com/gogf/gf/v2/test/gtest" @@ -19,7 +18,7 @@ import ( func Test_Config(t *testing.T) { gtest.C(t, func(t *gtest.T) { config := gview.Config{ - Paths: []string{gdebug.TestDataPath("config")}, + Paths: []string{gtest.DataPath("config")}, Data: g.Map{ "name": "gf", }, @@ -28,16 +27,16 @@ func Test_Config(t *testing.T) { } view := gview.New() err := view.SetConfig(config) - t.Assert(err, nil) + t.AssertNil(err) str := `hello ${.name},version:${.version}` view.Assigns(g.Map{"version": "1.7.0"}) result, err := view.ParseContent(context.TODO(), str, nil) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(result, "hello gf,version:1.7.0") result, err = view.ParseDefault(context.TODO()) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(result, "name:gf") }) } @@ -46,23 +45,23 @@ func Test_ConfigWithMap(t *testing.T) { gtest.C(t, func(t *gtest.T) { view := gview.New() err := view.SetConfigWithMap(g.Map{ - "Paths": []string{gdebug.TestDataPath("config")}, + "Paths": []string{gtest.DataPath("config")}, "DefaultFile": "test.html", "Delimiters": []string{"${", "}"}, "Data": g.Map{ "name": "gf", }, }) - t.Assert(err, nil) + t.AssertNil(err) str := `hello ${.name},version:${.version}` view.Assigns(g.Map{"version": "1.7.0"}) result, err := view.ParseContent(context.TODO(), str, nil) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(result, "hello gf,version:1.7.0") result, err = view.ParseDefault(context.TODO()) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(result, "name:gf") }) } diff --git a/os/gview/gview_z_unit_feature_encode_test.go b/os/gview/gview_z_unit_feature_encode_test.go index dc7913de5..786f98a4b 100644 --- a/os/gview/gview_z_unit_feature_encode_test.go +++ b/os/gview/gview_z_unit_feature_encode_test.go @@ -10,7 +10,6 @@ import ( "context" "testing" - "github.com/gogf/gf/v2/debug/gdebug" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gfile" "github.com/gogf/gf/v2/os/gview" @@ -20,12 +19,12 @@ import ( func Test_Encode_Parse(t *testing.T) { gtest.C(t, func(t *gtest.T) { v := gview.New() - v.SetPath(gdebug.TestDataPath("tpl")) + v.SetPath(gtest.DataPath("tpl")) v.SetAutoEncode(true) result, err := v.Parse(context.TODO(), "encode.tpl", g.Map{ "title": "my title", }) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(result, "
<b>my title</b>
") }) } @@ -33,12 +32,12 @@ func Test_Encode_Parse(t *testing.T) { func Test_Encode_ParseContent(t *testing.T) { gtest.C(t, func(t *gtest.T) { v := gview.New() - tplContent := gfile.GetContents(gdebug.TestDataPath("tpl", "encode.tpl")) + tplContent := gfile.GetContents(gtest.DataPath("tpl", "encode.tpl")) v.SetAutoEncode(true) result, err := v.ParseContent(context.TODO(), tplContent, g.Map{ "title": "my title", }) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(result, "
<b>my title</b>
") }) } diff --git a/os/gview/gview_z_unit_i18n_test.go b/os/gview/gview_z_unit_i18n_test.go index 58c1c27d5..dfd1e4026 100644 --- a/os/gview/gview_z_unit_i18n_test.go +++ b/os/gview/gview_z_unit_i18n_test.go @@ -23,27 +23,27 @@ func Test_I18n(t *testing.T) { expect2 := `john says "こんにちは世界!"` expect3 := `john says "{#hello}{#world}!"` - g.I18n().SetPath(gdebug.TestDataPath("i18n")) + g.I18n().SetPath(gtest.DataPath("i18n")) g.I18n().SetLanguage("zh-CN") result1, err := g.View().ParseContent(context.TODO(), content, g.Map{ "name": "john", }) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(result1, expect1) g.I18n().SetLanguage("ja") result2, err := g.View().ParseContent(context.TODO(), content, g.Map{ "name": "john", }) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(result2, expect2) g.I18n().SetLanguage("none") result3, err := g.View().ParseContent(context.TODO(), content, g.Map{ "name": "john", }) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(result3, expect3) }) gtest.C(t, func(t *gtest.T) { @@ -58,21 +58,21 @@ func Test_I18n(t *testing.T) { "name": "john", "I18nLanguage": "zh-CN", }) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(result1, expect1) result2, err := g.View().ParseContent(context.TODO(), content, g.Map{ "name": "john", "I18nLanguage": "ja", }) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(result2, expect2) result3, err := g.View().ParseContent(context.TODO(), content, g.Map{ "name": "john", "I18nLanguage": "none", }) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(result3, expect3) }) } diff --git a/os/gview/gview_z_unit_test.go b/os/gview/gview_z_unit_test.go index 680c129a6..cfead2fc9 100644 --- a/os/gview/gview_z_unit_test.go +++ b/os/gview/gview_z_unit_test.go @@ -23,6 +23,7 @@ import ( "github.com/gogf/gf/v2/test/gtest" "github.com/gogf/gf/v2/text/gstr" "github.com/gogf/gf/v2/util/gconv" + "github.com/gogf/gf/v2/util/guid" ) func init() { @@ -174,7 +175,7 @@ func Test_Func(t *testing.T) { str = `{{concat "I" "Love" "GoFrame"}}` result, err = gview.ParseContent(context.TODO(), str, nil) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(result, `ILoveGoFrame`) }) // eq: multiple values. @@ -196,7 +197,7 @@ func Test_FuncNl2Br(t *testing.T) { gtest.C(t, func(t *gtest.T) { str := `{{"Go\nFrame" | nl2br}}` result, err := gview.ParseContent(context.TODO(), str, nil) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(result, `Go
Frame`) }) gtest.C(t, func(t *gtest.T) { @@ -208,7 +209,7 @@ func Test_FuncNl2Br(t *testing.T) { result, err := gview.ParseContent(context.TODO(), str, g.Map{ "content": s, }) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(result, strings.Replace(s, "\n", "
", -1)) }) } @@ -222,7 +223,7 @@ func Test_FuncInclude(t *testing.T) { layout = `{{include "header.html" .}} {{include "main.html" .}} {{include "footer.html" .}}` - templatePath = gfile.TempDir("template") + templatePath = gfile.Temp(guid.S()) ) gfile.Mkdir(templatePath) @@ -294,7 +295,7 @@ func Test_ParseContent(t *testing.T) { func Test_HotReload(t *testing.T) { gtest.C(t, func(t *gtest.T) { dirPath := gfile.Join( - gfile.TempDir(), + gfile.Temp(), "testdata", "template-"+gconv.String(gtime.TimestampNano()), ) @@ -303,7 +304,7 @@ func Test_HotReload(t *testing.T) { // Initialize data. err := gfile.PutContents(filePath, "test:{{.var}}") - t.Assert(err, nil) + t.AssertNil(err) view := gview.New(dirPath) @@ -311,18 +312,18 @@ func Test_HotReload(t *testing.T) { result, err := view.Parse(context.TODO(), "test.html", g.Map{ "var": "1", }) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(result, `test:1`) // Update data. err = gfile.PutContents(filePath, "test2:{{.var}}") - t.Assert(err, nil) + t.AssertNil(err) time.Sleep(100 * time.Millisecond) result, err = view.Parse(context.TODO(), "test.html", g.Map{ "var": "2", }) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(result, `test2:2`) }) } @@ -334,7 +335,7 @@ func Test_XSS(t *testing.T) { r, err := v.ParseContent(context.TODO(), "{{.v}}", g.Map{ "v": s, }) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(r, s) }) gtest.C(t, func(t *gtest.T) { @@ -344,7 +345,7 @@ func Test_XSS(t *testing.T) { r, err := v.ParseContent(context.TODO(), "{{.v}}", g.Map{ "v": s, }) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(r, ghtml.Entities(s)) }) // Tag "if". @@ -355,7 +356,7 @@ func Test_XSS(t *testing.T) { r, err := v.ParseContent(context.TODO(), "{{if eq 1 1}}{{.v}}{{end}}", g.Map{ "v": s, }) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(r, ghtml.Entities(s)) }) } @@ -374,7 +375,7 @@ func Test_BuildInFuncMap(t *testing.T) { v := gview.New() v.Assign("v", new(TypeForBuildInFuncMap)) r, err := v.ParseContent(context.TODO(), "{{range $k, $v := map .v.Test}} {{$k}}:{{$v}} {{end}}") - t.Assert(err, nil) + t.AssertNil(err) t.Assert(gstr.Contains(r, "Name:john"), true) t.Assert(gstr.Contains(r, "Score:99.9"), true) }) @@ -397,7 +398,7 @@ func Test_BuildInFuncMaps(t *testing.T) { v := gview.New() v.Assign("v", new(TypeForBuildInFuncMaps)) r, err := v.ParseContent(context.TODO(), "{{range $k, $v := maps .v.Test}} {{$k}}:{{$v.Name}} {{$v.Score}} {{end}}") - t.Assert(err, nil) + t.AssertNil(err) t.Assert(r, ` 0:john 99.9 1:smith 100 `) }) } @@ -410,7 +411,7 @@ func Test_BuildInFuncDump(t *testing.T) { "score": 100, }) r, err := v.ParseContent(context.TODO(), "{{dump .}}") - t.Assert(err, nil) + t.AssertNil(err) fmt.Println(r) t.Assert(gstr.Contains(r, `"name": "john"`), true) t.Assert(gstr.Contains(r, `"score": 100`), true) @@ -424,16 +425,80 @@ func Test_BuildInFuncJson(t *testing.T) { "name": "john", }) r, err := v.ParseContent(context.TODO(), "{{json .v}}") - t.Assert(err, nil) + t.AssertNil(err) t.Assert(r, `{"name":"john"}`) }) } +func Test_BuildInFuncXml(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + v := gview.New() + v.Assign("v", g.Map{ + "name": "john", + }) + r, err := v.ParseContent(context.TODO(), "{{xml .v}}") + t.AssertNil(err) + t.Assert(r, `john`) + }) +} + +func Test_BuildInFuncIni(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + v := gview.New() + v.Assign("v", g.Map{ + "name": "john", + }) + r, err := v.ParseContent(context.TODO(), "{{ini .v}}") + t.AssertNil(err) + t.Assert(r, `name=john +`) + }) +} + +func Test_BuildInFuncYaml(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + v := gview.New() + v.Assign("v", g.Map{ + "name": "john", + }) + r, err := v.ParseContent(context.TODO(), "{{yaml .v}}") + t.AssertNil(err) + t.Assert(r, `name: john +`) + }) +} + +func Test_BuildInFuncYamlIndent(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + v := gview.New() + v.Assign("v", g.Map{ + "name": "john", + }) + r, err := v.ParseContent(context.TODO(), `{{yamli .v "####"}}`) + t.AssertNil(err) + t.Assert(r, `####name: john +`) + }) +} + +func Test_BuildInFuncToml(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + v := gview.New() + v.Assign("v", g.Map{ + "name": "john", + }) + r, err := v.ParseContent(context.TODO(), "{{toml .v}}") + t.AssertNil(err) + t.Assert(r, `name = "john" +`) + }) +} + func Test_BuildInFuncPlus(t *testing.T) { gtest.C(t, func(t *gtest.T) { v := gview.New() r, err := v.ParseContent(gctx.New(), "{{plus 1 2 3}}") - t.Assert(err, nil) + t.AssertNil(err) t.Assert(r, `6`) }) } @@ -442,7 +507,7 @@ func Test_BuildInFuncMinus(t *testing.T) { gtest.C(t, func(t *gtest.T) { v := gview.New() r, err := v.ParseContent(gctx.New(), "{{minus 1 2 3}}") - t.Assert(err, nil) + t.AssertNil(err) t.Assert(r, `-4`) }) } @@ -451,7 +516,7 @@ func Test_BuildInFuncTimes(t *testing.T) { gtest.C(t, func(t *gtest.T) { v := gview.New() r, err := v.ParseContent(gctx.New(), "{{times 1 2 3 4}}") - t.Assert(err, nil) + t.AssertNil(err) t.Assert(r, `24`) }) } @@ -460,7 +525,24 @@ func Test_BuildInFuncDivide(t *testing.T) { gtest.C(t, func(t *gtest.T) { v := gview.New() r, err := v.ParseContent(gctx.New(), "{{divide 8 2 2}}") - t.Assert(err, nil) + t.AssertNil(err) t.Assert(r, `2`) }) } + +func Test_Issue1416(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + v := gview.New() + err := v.SetPath(gtest.DataPath("issue1416")) + t.AssertNil(err) + r, err := v.ParseOption(context.TODO(), gview.Option{ + File: "gview.tpl", + Orphan: true, + Params: map[string]interface{}{ + "hello": "world", + }, + }) + t.AssertNil(err) + t.Assert(r, `test.tpl content, vars: world`) + }) +} diff --git a/os/gview/testdata/issue1416/gview copy.tpl b/os/gview/testdata/issue1416/gview copy.tpl new file mode 100644 index 000000000..97b8f75cc --- /dev/null +++ b/os/gview/testdata/issue1416/gview copy.tpl @@ -0,0 +1 @@ +test.tpl content, vars: {{ hello }} \ No newline at end of file diff --git a/os/gview/testdata/issue1416/gview.tpl b/os/gview/testdata/issue1416/gview.tpl new file mode 100644 index 000000000..c31b46da5 --- /dev/null +++ b/os/gview/testdata/issue1416/gview.tpl @@ -0,0 +1 @@ +test.tpl content, vars: {{.hello}} \ No newline at end of file diff --git a/protocol/goai/goai.go b/protocol/goai/goai.go index c8cbb9148..aef00be0e 100644 --- a/protocol/goai/goai.go +++ b/protocol/goai/goai.go @@ -25,25 +25,18 @@ import ( // https://swagger.io/specification/ // https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.0.md type OpenApiV3 struct { - Config Config `json:"-" yaml:"-"` - OpenAPI string `json:"openapi" yaml:"openapi"` - Components Components `json:"components,omitempty" yaml:"components,omitempty"` - Info Info `json:"info" yaml:"info"` - Paths Paths `json:"paths" yaml:"paths"` - Security *SecurityRequirements `json:"security,omitempty" yaml:"security,omitempty"` - Servers *Servers `json:"servers,omitempty" yaml:"servers,omitempty"` - Tags *Tags `json:"tags,omitempty" yaml:"tags,omitempty"` - ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` -} - -// ExternalDocs is specified by OpenAPI/Swagger standard version 3.0. -type ExternalDocs struct { - URL string `json:"url,omitempty"` - Description string `json:"description,omitempty"` + Config Config `json:"-"` + OpenAPI string `json:"openapi"` + Components Components `json:"components,omitempty"` + Info Info `json:"info"` + Paths Paths `json:"paths"` + Security *SecurityRequirements `json:"security,omitempty"` + Servers *Servers `json:"servers,omitempty"` + Tags *Tags `json:"tags,omitempty"` + ExternalDocs *ExternalDocs `json:"externalDocs,omitempty"` } const ( - HttpMethodAll = `ALL` HttpMethodGet = `GET` HttpMethodPut = `PUT` HttpMethodPost = `POST` @@ -56,6 +49,7 @@ const ( ) const ( + TypeInteger = `integer` TypeNumber = `number` TypeBoolean = `boolean` TypeArray = `array` @@ -82,22 +76,28 @@ const ( TagNamePath = `path` TagNameMethod = `method` TagNameMime = `mime` + TagNameConsumes = `consumes` TagNameType = `type` TagNameDomain = `domain` - TagNameValidate = `v` +) + +const ( + patternKeyForRequired = `required` + patternKeyForIn = `in:` ) var ( defaultReadContentTypes = []string{`application/json`} defaultWriteContentTypes = []string{`application/json`} shortTypeMapForTag = map[string]string{ - "d": "default", - "sum": "summary", - "sm": "summary", - "des": "description", - "dc": "description", - "eg": "example", - "egs": "examples", + "d": "Default", + "sum": "Summary", + "sm": "Summary", + "des": "Description", + "dc": "Description", + "eg": "Example", + "egs": "Examples", + "ed": "ExternalDocs", } ) @@ -148,7 +148,7 @@ func (oai *OpenApiV3) Add(in AddInput) error { func (oai OpenApiV3) String() string { b, err := json.Marshal(oai) if err != nil { - intlog.Error(context.TODO(), err) + intlog.Errorf(context.TODO(), `%+v`, err) } return string(b) } @@ -180,7 +180,10 @@ func (oai *OpenApiV3) golangTypeToOAIType(t reflect.Type) string { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, - reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return TypeInteger + + case reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128: return TypeNumber @@ -237,3 +240,10 @@ func (oai *OpenApiV3) fileMapWithShortTags(m map[string]string) map[string]strin func formatRefToBytes(ref string) []byte { return []byte(fmt.Sprintf(`{"$ref":"#/components/schemas/%s"}`, ref)) } + +func isValidParameterName(key string) bool { + if key == "-" { + return false + } + return true +} diff --git a/protocol/goai/goai_components.go b/protocol/goai/goai_components.go index 465972c84..85a47f0b9 100644 --- a/protocol/goai/goai_components.go +++ b/protocol/goai/goai_components.go @@ -8,15 +8,15 @@ package goai // Components is specified by OpenAPI/Swagger standard version 3.0. type Components struct { - Schemas Schemas `json:"schemas,omitempty" yaml:"schemas,omitempty"` - Parameters ParametersMap `json:"parameters,omitempty" yaml:"parameters,omitempty"` - Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"` - RequestBodies RequestBodies `json:"requestBodies,omitempty" yaml:"requestBodies,omitempty"` - Responses Responses `json:"responses,omitempty" yaml:"responses,omitempty"` - SecuritySchemes SecuritySchemes `json:"securitySchemes,omitempty" yaml:"securitySchemes,omitempty"` - Examples Examples `json:"examples,omitempty" yaml:"examples,omitempty"` - Links Links `json:"links,omitempty" yaml:"links,omitempty"` - Callbacks Callbacks `json:"callbacks,omitempty" yaml:"callbacks,omitempty"` + Schemas Schemas `json:"schemas,omitempty"` + Parameters ParametersMap `json:"parameters,omitempty"` + Headers Headers `json:"headers,omitempty"` + RequestBodies RequestBodies `json:"requestBodies,omitempty"` + Responses Responses `json:"responses,omitempty"` + SecuritySchemes SecuritySchemes `json:"securitySchemes,omitempty"` + Examples Examples `json:"examples,omitempty"` + Links Links `json:"links,omitempty"` + Callbacks Callbacks `json:"callbacks,omitempty"` } type ParametersMap map[string]*ParameterRef diff --git a/protocol/goai/goai_example.go b/protocol/goai/goai_example.go index 8af3005b1..61b75d4fb 100644 --- a/protocol/goai/goai_example.go +++ b/protocol/goai/goai_example.go @@ -12,10 +12,10 @@ import ( // Example is specified by OpenAPI/Swagger 3.0 standard. type Example struct { - Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Value interface{} `json:"value,omitempty" yaml:"value,omitempty"` - ExternalValue string `json:"externalValue,omitempty" yaml:"externalValue,omitempty"` + Summary string `json:"summary,omitempty"` + Description string `json:"description,omitempty"` + Value interface{} `json:"value,omitempty"` + ExternalValue string `json:"externalValue,omitempty"` } type Examples map[string]*ExampleRef diff --git a/protocol/goai/goai_external_docs.go b/protocol/goai/goai_external_docs.go new file mode 100644 index 000000000..2265865c0 --- /dev/null +++ b/protocol/goai/goai_external_docs.go @@ -0,0 +1,35 @@ +// Copyright GoFrame Author(https://goframe.org). 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 goai + +import ( + "github.com/gogf/gf/v2/internal/json" + "github.com/gogf/gf/v2/text/gstr" + "github.com/gogf/gf/v2/util/gconv" +) + +// ExternalDocs is specified by OpenAPI/Swagger standard version 3.0. +type ExternalDocs struct { + URL string `json:"url,omitempty"` + Description string `json:"description,omitempty"` +} + +func (ed *ExternalDocs) UnmarshalValue(value interface{}) error { + var valueBytes = gconv.Bytes(value) + if json.Valid(valueBytes) { + return json.UnmarshalUseNumber(valueBytes, ed) + } + var ( + valueString = string(valueBytes) + valueArray = gstr.Split(valueString, "|") + ) + ed.URL = valueArray[0] + if len(valueArray) > 1 { + ed.Description = valueArray[1] + } + return nil +} diff --git a/protocol/goai/goai_info.go b/protocol/goai/goai_info.go index 3c0738e67..91709e3d8 100644 --- a/protocol/goai/goai_info.go +++ b/protocol/goai/goai_info.go @@ -8,23 +8,23 @@ package goai // Info is specified by OpenAPI/Swagger standard version 3.0. type Info struct { - Title string `json:"title" yaml:"title"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - TermsOfService string `json:"termsOfService,omitempty" yaml:"termsOfService,omitempty"` - Contact *Contact `json:"contact,omitempty" yaml:"contact,omitempty"` - License *License `json:"license,omitempty" yaml:"license,omitempty"` - Version string `json:"version" yaml:"version"` + Title string `json:"title"` + Description string `json:"description,omitempty"` + TermsOfService string `json:"termsOfService,omitempty"` + Contact *Contact `json:"contact,omitempty"` + License *License `json:"license,omitempty"` + Version string `json:"version"` } // Contact is specified by OpenAPI/Swagger standard version 3.0. type Contact struct { - Name string `json:"name,omitempty" yaml:"name,omitempty"` - URL string `json:"url,omitempty" yaml:"url,omitempty"` - Email string `json:"email,omitempty" yaml:"email,omitempty"` + Name string `json:"name,omitempty"` + URL string `json:"url,omitempty"` + Email string `json:"email,omitempty"` } // License is specified by OpenAPI/Swagger standard version 3.0. type License struct { - Name string `json:"name" yaml:"name"` - URL string `json:"url,omitempty" yaml:"url,omitempty"` + Name string `json:"name"` + URL string `json:"url,omitempty"` } diff --git a/protocol/goai/goai_link.go b/protocol/goai/goai_link.go index 261fa51cc..0968c1b71 100644 --- a/protocol/goai/goai_link.go +++ b/protocol/goai/goai_link.go @@ -12,12 +12,12 @@ import ( // Link is specified by OpenAPI/Swagger standard version 3.0. type Link struct { - OperationID string `json:"operationId,omitempty" yaml:"operationId,omitempty"` - OperationRef string `json:"operationRef,omitempty" yaml:"operationRef,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Parameters map[string]interface{} `json:"parameters,omitempty" yaml:"parameters,omitempty"` - Server *Server `json:"server,omitempty" yaml:"server,omitempty"` - RequestBody interface{} `json:"requestBody,omitempty" yaml:"requestBody,omitempty"` + OperationID string `json:"operationId,omitempty"` + OperationRef string `json:"operationRef,omitempty"` + Description string `json:"description,omitempty"` + Parameters map[string]interface{} `json:"parameters,omitempty"` + Server *Server `json:"server,omitempty"` + RequestBody interface{} `json:"requestBody,omitempty"` } type Links map[string]LinkRef diff --git a/protocol/goai/goai_mediatype.go b/protocol/goai/goai_mediatype.go index d5af61dba..4c2257df1 100644 --- a/protocol/goai/goai_mediatype.go +++ b/protocol/goai/goai_mediatype.go @@ -8,10 +8,10 @@ package goai // MediaType is specified by OpenAPI/Swagger 3.0 standard. type MediaType struct { - Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` - Example interface{} `json:"example,omitempty" yaml:"example,omitempty"` - Examples Examples `json:"examples,omitempty" yaml:"examples,omitempty"` - Encoding map[string]*Encoding `json:"encoding,omitempty" yaml:"encoding,omitempty"` + Schema *SchemaRef `json:"schema,omitempty"` + Example interface{} `json:"example,omitempty"` + Examples Examples `json:"examples,omitempty"` + Encoding map[string]*Encoding `json:"encoding,omitempty"` } // Content is specified by OpenAPI/Swagger 3.0 standard. @@ -19,9 +19,9 @@ type Content map[string]MediaType // Encoding is specified by OpenAPI/Swagger 3.0 standard. type Encoding struct { - ContentType string `json:"contentType,omitempty" yaml:"contentType,omitempty"` - Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"` - Style string `json:"style,omitempty" yaml:"style,omitempty"` - Explode *bool `json:"explode,omitempty" yaml:"explode,omitempty"` - AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"` + ContentType string `json:"contentType,omitempty"` + Headers Headers `json:"headers,omitempty"` + Style string `json:"style,omitempty"` + Explode *bool `json:"explode,omitempty"` + AllowReserved bool `json:"allowReserved,omitempty"` } diff --git a/protocol/goai/goai_operation.go b/protocol/goai/goai_operation.go index 2a896f79e..db6e0b30d 100644 --- a/protocol/goai/goai_operation.go +++ b/protocol/goai/goai_operation.go @@ -6,18 +6,56 @@ package goai +import ( + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/internal/json" + "github.com/gogf/gf/v2/util/gconv" +) + // Operation represents "operation" specified by OpenAPI/Swagger 3.0 standard. type Operation struct { - Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"` - Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - OperationID string `json:"operationId,omitempty" yaml:"operationId,omitempty"` - Parameters Parameters `json:"parameters,omitempty" yaml:"parameters,omitempty"` - RequestBody *RequestBodyRef `json:"requestBody,omitempty" yaml:"requestBody,omitempty"` - Responses Responses `json:"responses" yaml:"responses"` - Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` - Callbacks *Callbacks `json:"callbacks,omitempty" yaml:"callbacks,omitempty"` - Security *SecurityRequirements `json:"security,omitempty" yaml:"security,omitempty"` - Servers *Servers `json:"servers,omitempty" yaml:"servers,omitempty"` - ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` + Tags []string `json:"tags,omitempty"` + Summary string `json:"summary,omitempty"` + Description string `json:"description,omitempty"` + OperationID string `json:"operationId,omitempty"` + Parameters Parameters `json:"parameters,omitempty"` + RequestBody *RequestBodyRef `json:"requestBody,omitempty"` + Responses Responses `json:"responses"` + Deprecated bool `json:"deprecated,omitempty"` + Callbacks *Callbacks `json:"callbacks,omitempty"` + Security *SecurityRequirements `json:"security,omitempty"` + Servers *Servers `json:"servers,omitempty"` + ExternalDocs *ExternalDocs `json:"externalDocs,omitempty"` + XExtensions XExtensions `json:"-"` +} + +func (oai *OpenApiV3) tagMapToOperation(tagMap map[string]string, operation *Operation) error { + var mergedTagMap = oai.fileMapWithShortTags(tagMap) + if err := gconv.Struct(mergedTagMap, operation); err != nil { + return gerror.Wrap(err, `mapping struct tags to Operation failed`) + } + oai.tagMapToXExtensions(mergedTagMap, operation.XExtensions) + return nil +} + +func (o Operation) MarshalJSON() ([]byte, error) { + var ( + b []byte + m map[string]json.RawMessage + err error + ) + type tempOperation Operation // To prevent JSON marshal recursion error. + if b, err = json.Marshal(tempOperation(o)); err != nil { + return nil, err + } + if err = json.Unmarshal(b, &m); err != nil { + return nil, err + } + for k, v := range o.XExtensions { + if b, err = json.Marshal(v); err != nil { + return nil, err + } + m[k] = b + } + return json.Marshal(m) } diff --git a/protocol/goai/goai_parameter.go b/protocol/goai/goai_parameter.go index 082bd60e4..d20e92cdc 100644 --- a/protocol/goai/goai_parameter.go +++ b/protocol/goai/goai_parameter.go @@ -7,104 +7,57 @@ package goai import ( - "fmt" - - "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/internal/json" - "github.com/gogf/gf/v2/os/gstructs" - "github.com/gogf/gf/v2/text/gstr" "github.com/gogf/gf/v2/util/gconv" ) // Parameter is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.0.md#parameterObject type Parameter struct { - Name string `json:"name,omitempty" yaml:"name,omitempty"` - In string `json:"in,omitempty" yaml:"in,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Style string `json:"style,omitempty" yaml:"style,omitempty"` - Explode *bool `json:"explode,omitempty" yaml:"explode,omitempty"` - AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` - AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"` - Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` - Required bool `json:"required,omitempty" yaml:"required,omitempty"` - Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` - Example interface{} `json:"example,omitempty" yaml:"example,omitempty"` - Examples *Examples `json:"examples,omitempty" yaml:"examples,omitempty"` - Content *Content `json:"content,omitempty" yaml:"content,omitempty"` + Name string `json:"name,omitempty"` + In string `json:"in,omitempty"` + Description string `json:"description,omitempty"` + Style string `json:"style,omitempty"` + Explode *bool `json:"explode,omitempty"` + AllowEmptyValue bool `json:"allowEmptyValue,omitempty"` + AllowReserved bool `json:"allowReserved,omitempty"` + Deprecated bool `json:"deprecated,omitempty"` + Required bool `json:"required,omitempty"` + Schema *SchemaRef `json:"schema,omitempty"` + Example interface{} `json:"example,omitempty"` + Examples *Examples `json:"examples,omitempty"` + Content *Content `json:"content,omitempty"` + XExtensions XExtensions `json:"-"` } -// Parameters is specified by OpenAPI/Swagger 3.0 standard. -type Parameters []ParameterRef - -type ParameterRef struct { - Ref string - Value *Parameter +func (oai *OpenApiV3) tagMapToParameter(tagMap map[string]string, parameter *Parameter) error { + var mergedTagMap = oai.fileMapWithShortTags(tagMap) + if err := gconv.Struct(mergedTagMap, parameter); err != nil { + return gerror.Wrap(err, `mapping struct tags to Parameter failed`) + } + oai.tagMapToXExtensions(mergedTagMap, parameter.XExtensions) + return nil } -func (oai *OpenApiV3) newParameterRefWithStructMethod(field gstructs.Field, path, method string) (*ParameterRef, error) { +func (p Parameter) MarshalJSON() ([]byte, error) { var ( - tagMap = field.TagMap() - parameter = &Parameter{ - Name: field.TagJsonName(), - } + b []byte + m map[string]json.RawMessage + err error ) - if parameter.Name == "" { - parameter.Name = field.Name() - } - if len(tagMap) > 0 { - err := gconv.Struct(oai.fileMapWithShortTags(tagMap), parameter) - if err != nil { - return nil, gerror.Wrap(err, `mapping struct tags to Parameter failed`) - } - } - if parameter.In == "" { - // Automatically detect its "in" attribute. - if gstr.ContainsI(path, fmt.Sprintf(`{%s}`, parameter.Name)) { - parameter.In = ParameterInPath - } else { - // Default the parameter input to "query" if method is "GET/DELETE". - switch gstr.ToUpper(method) { - case HttpMethodGet, HttpMethodDelete: - parameter.In = ParameterInQuery - - default: - return nil, nil - } - } - } - - switch parameter.In { - case ParameterInPath: - // Required for path parameter. - parameter.Required = true - - case ParameterInCookie, ParameterInHeader, ParameterInQuery: - // Check validation tag. - if validateTagValue := field.Tag(TagNameValidate); gstr.ContainsI(validateTagValue, `required`) { - parameter.Required = true - } - - default: - return nil, gerror.NewCodef(gcode.CodeInvalidParameter, `invalid tag value "%s" for In`, parameter.In) - } - // Necessary schema or content. - schemaRef, err := oai.newSchemaRefWithGolangType(field.Type().Type, tagMap) - if err != nil { + type tempParameter Parameter // To prevent JSON marshal recursion error. + if b, err = json.Marshal(tempParameter(p)); err != nil { return nil, err } - parameter.Schema = schemaRef - - return &ParameterRef{ - Ref: "", - Value: parameter, - }, nil -} - -func (r ParameterRef) MarshalJSON() ([]byte, error) { - if r.Ref != "" { - return formatRefToBytes(r.Ref), nil + if err = json.Unmarshal(b, &m); err != nil { + return nil, err } - return json.Marshal(r.Value) + for k, v := range p.XExtensions { + if b, err = json.Marshal(v); err != nil { + return nil, err + } + m[k] = b + } + return json.Marshal(m) } diff --git a/protocol/goai/goai_parameter_ref.go b/protocol/goai/goai_parameter_ref.go new file mode 100644 index 000000000..416ec7dcc --- /dev/null +++ b/protocol/goai/goai_parameter_ref.go @@ -0,0 +1,100 @@ +// Copyright GoFrame Author(https://goframe.org). 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 goai + +import ( + "fmt" + + "github.com/gogf/gf/v2/container/gset" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/internal/json" + "github.com/gogf/gf/v2/os/gstructs" + "github.com/gogf/gf/v2/text/gstr" +) + +// Parameters is specified by OpenAPI/Swagger 3.0 standard. +type Parameters []ParameterRef + +type ParameterRef struct { + Ref string + Value *Parameter +} + +func (oai *OpenApiV3) newParameterRefWithStructMethod(field gstructs.Field, path, method string) (*ParameterRef, error) { + var ( + tagMap = field.TagMap() + parameter = &Parameter{ + Name: field.TagJsonName(), + XExtensions: make(XExtensions), + } + ) + if parameter.Name == "" { + parameter.Name = field.Name() + } + if len(tagMap) > 0 { + if err := oai.tagMapToParameter(tagMap, parameter); err != nil { + return nil, err + } + } + if parameter.In == "" { + // Automatically detect its "in" attribute. + if gstr.ContainsI(path, fmt.Sprintf(`{%s}`, parameter.Name)) { + parameter.In = ParameterInPath + } else { + // Default the parameter input to "query" if method is "GET/DELETE". + switch gstr.ToUpper(method) { + case HttpMethodGet, HttpMethodDelete: + parameter.In = ParameterInQuery + + default: + return nil, nil + } + } + } + + switch parameter.In { + case ParameterInPath: + // Required for path parameter. + parameter.Required = true + + case ParameterInCookie, ParameterInHeader, ParameterInQuery: + + default: + return nil, gerror.NewCodef(gcode.CodeInvalidParameter, `invalid tag value "%s" for In`, parameter.In) + } + // Necessary schema or content. + schemaRef, err := oai.newSchemaRefWithGolangType(field.Type().Type, tagMap) + if err != nil { + return nil, err + } + parameter.Schema = schemaRef + + // Ignore parameter. + if !isValidParameterName(parameter.Name) { + return nil, nil + } + + // Required check. + if parameter.Schema.Value != nil && parameter.Schema.Value.Pattern != "" { + if gset.NewStrSetFrom(gstr.Split(parameter.Schema.Value.Pattern, "|")).Contains(patternKeyForRequired) { + parameter.Required = true + } + } + + return &ParameterRef{ + Ref: "", + Value: parameter, + }, nil +} + +func (r ParameterRef) MarshalJSON() ([]byte, error) { + if r.Ref != "" { + return formatRefToBytes(r.Ref), nil + } + return json.Marshal(r.Value) +} diff --git a/protocol/goai/goai_path.go b/protocol/goai/goai_path.go index 4f9125f82..d5d8af66f 100644 --- a/protocol/goai/goai_path.go +++ b/protocol/goai/goai_path.go @@ -11,6 +11,7 @@ import ( "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/internal/json" "github.com/gogf/gf/v2/os/gstructs" "github.com/gogf/gf/v2/text/gstr" "github.com/gogf/gf/v2/util/gconv" @@ -18,20 +19,21 @@ import ( ) type Path struct { - Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` - Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Connect *Operation `json:"connect,omitempty" yaml:"connect,omitempty"` - Delete *Operation `json:"delete,omitempty" yaml:"delete,omitempty"` - Get *Operation `json:"get,omitempty" yaml:"get,omitempty"` - Head *Operation `json:"head,omitempty" yaml:"head,omitempty"` - Options *Operation `json:"options,omitempty" yaml:"options,omitempty"` - Patch *Operation `json:"patch,omitempty" yaml:"patch,omitempty"` - Post *Operation `json:"post,omitempty" yaml:"post,omitempty"` - Put *Operation `json:"put,omitempty" yaml:"put,omitempty"` - Trace *Operation `json:"trace,omitempty" yaml:"trace,omitempty"` - Servers Servers `json:"servers,omitempty" yaml:"servers,omitempty"` - Parameters Parameters `json:"parameters,omitempty" yaml:"parameters,omitempty"` + Ref string `json:"$ref,omitempty"` + Summary string `json:"summary,omitempty"` + Description string `json:"description,omitempty"` + Connect *Operation `json:"connect,omitempty"` + Delete *Operation `json:"delete,omitempty"` + Get *Operation `json:"get,omitempty"` + Head *Operation `json:"head,omitempty"` + Options *Operation `json:"options,omitempty"` + Patch *Operation `json:"patch,omitempty"` + Post *Operation `json:"post,omitempty"` + Put *Operation `json:"put,omitempty"` + Trace *Operation `json:"trace,omitempty"` + Servers Servers `json:"servers,omitempty"` + Parameters Parameters `json:"parameters,omitempty"` + XExtensions XExtensions `json:"-"` } // Paths are specified by OpenAPI/Swagger standard version 3.0. @@ -80,14 +82,16 @@ func (oai *OpenApiV3) addPath(in addPathInput) error { } var ( - path = Path{} + mime string + path = Path{XExtensions: make(XExtensions)} inputMetaMap = gmeta.Data(inputObject.Interface()) outputMetaMap = gmeta.Data(outputObject.Interface()) isInputStructEmpty = oai.doesStructHasNoFields(inputObject.Interface()) inputStructTypeName = oai.golangTypeToSchemaName(inputObject.Type()) outputStructTypeName = oai.golangTypeToSchemaName(outputObject.Type()) operation = Operation{ - Responses: map[string]ResponseRef{}, + Responses: map[string]ResponseRef{}, + XExtensions: make(XExtensions), } ) // Path check. @@ -126,11 +130,15 @@ func (oai *OpenApiV3) addPath(in addPathInput) error { } if len(inputMetaMap) > 0 { - if err := gconv.Struct(oai.fileMapWithShortTags(inputMetaMap), &path); err != nil { - return gerror.Wrap(err, `mapping struct tags to Path failed`) + if err := oai.tagMapToPath(inputMetaMap, &path); err != nil { + return err } - if err := gconv.Struct(oai.fileMapWithShortTags(inputMetaMap), &operation); err != nil { - return gerror.Wrap(err, `mapping struct tags to Operation failed`) + if err := oai.tagMapToOperation(inputMetaMap, &operation); err != nil { + return err + } + // Allowed request mime. + if mime = inputMetaMap[TagNameMime]; mime == "" { + mime = inputMetaMap[TagNameConsumes] } } @@ -200,12 +208,13 @@ func (oai *OpenApiV3) addPath(in addPathInput) error { if _, ok := operation.Responses[responseOkKey]; !ok { var ( response = Response{ - Content: map[string]MediaType{}, + Content: map[string]MediaType{}, + XExtensions: make(XExtensions), } ) if len(outputMetaMap) > 0 { - if err := gconv.Struct(oai.fileMapWithShortTags(outputMetaMap), &response); err != nil { - return gerror.Wrap(err, `mapping struct tags to Response failed`) + if err := oai.tagMapToResponse(outputMetaMap, &response); err != nil { + return err } } // Supported mime types of response. @@ -213,9 +222,9 @@ func (oai *OpenApiV3) addPath(in addPathInput) error { contentTypes = oai.Config.ReadContentTypes tagMimeValue = gmeta.Get(outputObject.Interface(), TagNameMime).String() refInput = getResponseSchemaRefInput{ - BusinessStructName: outputStructTypeName, - ResponseObject: oai.Config.CommonResponse, - ResponseDataField: oai.Config.CommonResponseDataField, + BusinessStructName: outputStructTypeName, + CommonResponseObject: oai.Config.CommonResponse, + CommonResponseDataField: oai.Config.CommonResponseDataField, } ) if tagMimeValue != "" { @@ -224,8 +233,8 @@ func (oai *OpenApiV3) addPath(in addPathInput) error { for _, v := range contentTypes { // If customized response mime type, it then ignores common response feature. if tagMimeValue != "" { - refInput.ResponseObject = nil - refInput.ResponseDataField = "" + refInput.CommonResponseObject = nil + refInput.CommonResponseDataField = "" } schemaRef, err := oai.getResponseSchemaRef(refInput) if err != nil { @@ -281,3 +290,34 @@ func (oai *OpenApiV3) addPath(in addPathInput) error { func (oai *OpenApiV3) doesStructHasNoFields(s interface{}) bool { return reflect.TypeOf(s).NumField() == 0 } + +func (oai *OpenApiV3) tagMapToPath(tagMap map[string]string, path *Path) error { + var mergedTagMap = oai.fileMapWithShortTags(tagMap) + if err := gconv.Struct(mergedTagMap, path); err != nil { + return gerror.Wrap(err, `mapping struct tags to Path failed`) + } + oai.tagMapToXExtensions(mergedTagMap, path.XExtensions) + return nil +} + +func (p Path) MarshalJSON() ([]byte, error) { + var ( + b []byte + m map[string]json.RawMessage + err error + ) + type tempPath Path // To prevent JSON marshal recursion error. + if b, err = json.Marshal(tempPath(p)); err != nil { + return nil, err + } + if err = json.Unmarshal(b, &m); err != nil { + return nil, err + } + for k, v := range p.XExtensions { + if b, err = json.Marshal(v); err != nil { + return nil, err + } + m[k] = b + } + return json.Marshal(m) +} diff --git a/protocol/goai/goai_requestbody.go b/protocol/goai/goai_requestbody.go index e3c47aa81..353923ac0 100644 --- a/protocol/goai/goai_requestbody.go +++ b/protocol/goai/goai_requestbody.go @@ -16,9 +16,9 @@ import ( // RequestBody is specified by OpenAPI/Swagger 3.0 standard. type RequestBody struct { - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Required bool `json:"required,omitempty" yaml:"required,omitempty"` - Content Content `json:"content,omitempty" yaml:"content,omitempty"` + Description string `json:"description,omitempty"` + Required bool `json:"required,omitempty"` + Content Content `json:"content,omitempty"` } type RequestBodyRef struct { @@ -47,40 +47,42 @@ func (oai *OpenApiV3) getRequestSchemaRef(in getRequestSchemaRefInput) (*SchemaR } var ( - dataFieldsPartsArray = gstr.Split(in.RequestDataField, ".") - bizRequestStructSchemaRef, bizRequestStructSchemaRefExist = oai.Components.Schemas[in.BusinessStructName] - schema, err = oai.structToSchema(in.RequestObject) + dataFieldsPartsArray = gstr.Split(in.RequestDataField, ".") + bizRequestStructSchemaRef = oai.Components.Schemas.Get(in.BusinessStructName) + schema, err = oai.structToSchema(in.RequestObject) ) if err != nil { return nil, err } - if in.RequestDataField == "" && bizRequestStructSchemaRefExist { - for k, v := range bizRequestStructSchemaRef.Value.Properties { - schema.Properties[k] = v - } + if in.RequestDataField == "" && bizRequestStructSchemaRef != nil { + // Normal request. + bizRequestStructSchemaRef.Value.Properties.Iterator(func(key string, ref SchemaRef) bool { + schema.Properties.Set(key, ref) + return true + }) } else { + // Common request. structFields, _ := gstructs.Fields(gstructs.FieldsInput{ Pointer: in.RequestObject, RecursiveOption: gstructs.RecursiveOptionEmbeddedNoTag, }) for _, structField := range structFields { - var ( - fieldName = structField.Name() - ) + var fieldName = structField.Name() if jsonName := structField.TagJsonName(); jsonName != "" { fieldName = jsonName } switch len(dataFieldsPartsArray) { case 1: if structField.Name() == dataFieldsPartsArray[0] { - schema.Properties[fieldName] = bizRequestStructSchemaRef + if err = oai.tagMapToSchema(structField.TagMap(), bizRequestStructSchemaRef.Value); err != nil { + return nil, err + } + schema.Properties.Set(fieldName, *bizRequestStructSchemaRef) break } default: if structField.Name() == dataFieldsPartsArray[0] { - var ( - structFieldInstance = reflect.New(structField.Type().Type).Elem() - ) + var structFieldInstance = reflect.New(structField.Type().Type).Elem() schemaRef, err := oai.getRequestSchemaRef(getRequestSchemaRefInput{ BusinessStructName: in.BusinessStructName, RequestObject: structFieldInstance, @@ -89,13 +91,12 @@ func (oai *OpenApiV3) getRequestSchemaRef(in getRequestSchemaRefInput) (*SchemaR if err != nil { return nil, err } - schema.Properties[fieldName] = *schemaRef + schema.Properties.Set(fieldName, *schemaRef) break } } } } - return &SchemaRef{ Value: schema, }, nil diff --git a/protocol/goai/goai_response.go b/protocol/goai/goai_response.go index e85b04254..8961bb395 100644 --- a/protocol/goai/goai_response.go +++ b/protocol/goai/goai_response.go @@ -7,100 +7,47 @@ package goai import ( - "reflect" - + "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/internal/json" - "github.com/gogf/gf/v2/os/gstructs" - "github.com/gogf/gf/v2/text/gstr" + "github.com/gogf/gf/v2/util/gconv" ) // Response is specified by OpenAPI/Swagger 3.0 standard. type Response struct { - Description string `json:"description" yaml:"description"` - Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"` - Content Content `json:"content,omitempty" yaml:"content,omitempty"` - Links Links `json:"links,omitempty" yaml:"links,omitempty"` + Description string `json:"description"` + Headers Headers `json:"headers,omitempty"` + Content Content `json:"content,omitempty"` + Links Links `json:"links,omitempty"` + XExtensions XExtensions `json:"-"` } -// Responses is specified by OpenAPI/Swagger 3.0 standard. -type Responses map[string]ResponseRef - -type ResponseRef struct { - Ref string - Value *Response -} - -func (r ResponseRef) MarshalJSON() ([]byte, error) { - if r.Ref != "" { - return formatRefToBytes(r.Ref), nil +func (oai *OpenApiV3) tagMapToResponse(tagMap map[string]string, response *Response) error { + var mergedTagMap = oai.fileMapWithShortTags(tagMap) + if err := gconv.Struct(mergedTagMap, response); err != nil { + return gerror.Wrap(err, `mapping struct tags to Response failed`) } - return json.Marshal(r.Value) + oai.tagMapToXExtensions(mergedTagMap, response.XExtensions) + return nil } -type getResponseSchemaRefInput struct { - BusinessStructName string // The business struct name. - ResponseObject interface{} // Common response object. - ResponseDataField string // Common response data field. -} - -func (oai *OpenApiV3) getResponseSchemaRef(in getResponseSchemaRefInput) (*SchemaRef, error) { - if in.ResponseObject == nil { - return &SchemaRef{ - Ref: in.BusinessStructName, - }, nil - } - +func (r Response) MarshalJSON() ([]byte, error) { var ( - dataFieldsPartsArray = gstr.Split(in.ResponseDataField, ".") - bizResponseStructSchemaRef, bizResponseStructSchemaRefExist = oai.Components.Schemas[in.BusinessStructName] - schema, err = oai.structToSchema(in.ResponseObject) + b []byte + m map[string]json.RawMessage + err error ) - if err != nil { + type tempResponse Response // To prevent JSON marshal recursion error. + if b, err = json.Marshal(tempResponse(r)); err != nil { return nil, err } - if in.ResponseDataField == "" && bizResponseStructSchemaRefExist { - for k, v := range bizResponseStructSchemaRef.Value.Properties { - schema.Properties[k] = v - } - } else { - structFields, _ := gstructs.Fields(gstructs.FieldsInput{ - Pointer: in.ResponseObject, - RecursiveOption: gstructs.RecursiveOptionEmbeddedNoTag, - }) - for _, structField := range structFields { - var ( - fieldName = structField.Name() - ) - if jsonName := structField.TagJsonName(); jsonName != "" { - fieldName = jsonName - } - switch len(dataFieldsPartsArray) { - case 1: - if structField.Name() == dataFieldsPartsArray[0] { - schema.Properties[fieldName] = bizResponseStructSchemaRef - break - } - default: - if structField.Name() == dataFieldsPartsArray[0] { - var ( - structFieldInstance = reflect.New(structField.Type().Type).Elem() - ) - schemaRef, err := oai.getResponseSchemaRef(getResponseSchemaRefInput{ - BusinessStructName: in.BusinessStructName, - ResponseObject: structFieldInstance, - ResponseDataField: gstr.Join(dataFieldsPartsArray[1:], "."), - }) - if err != nil { - return nil, err - } - schema.Properties[fieldName] = *schemaRef - break - } - } - } + if err = json.Unmarshal(b, &m); err != nil { + return nil, err } - - return &SchemaRef{ - Value: schema, - }, nil + for k, v := range r.XExtensions { + if b, err = json.Marshal(v); err != nil { + return nil, err + } + m[k] = b + } + return json.Marshal(m) } diff --git a/protocol/goai/goai_response_ref.go b/protocol/goai/goai_response_ref.go new file mode 100644 index 000000000..495aada71 --- /dev/null +++ b/protocol/goai/goai_response_ref.go @@ -0,0 +1,101 @@ +// Copyright GoFrame Author(https://goframe.org). 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 goai + +import ( + "reflect" + + "github.com/gogf/gf/v2/internal/json" + "github.com/gogf/gf/v2/os/gstructs" + "github.com/gogf/gf/v2/text/gstr" +) + +type ResponseRef struct { + Ref string + Value *Response +} + +// Responses is specified by OpenAPI/Swagger 3.0 standard. +type Responses map[string]ResponseRef + +func (r ResponseRef) MarshalJSON() ([]byte, error) { + if r.Ref != "" { + return formatRefToBytes(r.Ref), nil + } + return json.Marshal(r.Value) +} + +type getResponseSchemaRefInput struct { + BusinessStructName string // The business struct name. + CommonResponseObject interface{} // Common response object. + CommonResponseDataField string // Common response data field. +} + +func (oai *OpenApiV3) getResponseSchemaRef(in getResponseSchemaRefInput) (*SchemaRef, error) { + if in.CommonResponseObject == nil { + return &SchemaRef{ + Ref: in.BusinessStructName, + }, nil + } + + var ( + dataFieldsPartsArray = gstr.Split(in.CommonResponseDataField, ".") + bizResponseStructSchemaRef = oai.Components.Schemas.Get(in.BusinessStructName) + schema, err = oai.structToSchema(in.CommonResponseObject) + ) + if err != nil { + return nil, err + } + if in.CommonResponseDataField == "" && bizResponseStructSchemaRef != nil { + // Normal response. + bizResponseStructSchemaRef.Value.Properties.Iterator(func(key string, ref SchemaRef) bool { + schema.Properties.Set(key, ref) + return true + }) + } else { + // Common response. + structFields, _ := gstructs.Fields(gstructs.FieldsInput{ + Pointer: in.CommonResponseObject, + RecursiveOption: gstructs.RecursiveOptionEmbeddedNoTag, + }) + for _, structField := range structFields { + var fieldName = structField.Name() + if jsonName := structField.TagJsonName(); jsonName != "" { + fieldName = jsonName + } + switch len(dataFieldsPartsArray) { + case 1: + if structField.Name() == dataFieldsPartsArray[0] { + if err = oai.tagMapToSchema(structField.TagMap(), bizResponseStructSchemaRef.Value); err != nil { + return nil, err + } + schema.Properties.Set(fieldName, *bizResponseStructSchemaRef) + break + } + default: + // Recursively creating common response object schema. + if structField.Name() == dataFieldsPartsArray[0] { + var structFieldInstance = reflect.New(structField.Type().Type).Elem() + schemaRef, err := oai.getResponseSchemaRef(getResponseSchemaRefInput{ + BusinessStructName: in.BusinessStructName, + CommonResponseObject: structFieldInstance, + CommonResponseDataField: gstr.Join(dataFieldsPartsArray[1:], "."), + }) + if err != nil { + return nil, err + } + schema.Properties.Set(fieldName, *schemaRef) + break + } + } + } + } + + return &SchemaRef{ + Value: schema, + }, nil +} diff --git a/protocol/goai/goai_security.go b/protocol/goai/goai_security.go index e636b4b04..ece9b8523 100644 --- a/protocol/goai/goai_security.go +++ b/protocol/goai/goai_security.go @@ -11,14 +11,14 @@ import ( ) type SecurityScheme struct { - Type string `json:"type,omitempty" yaml:"type,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Name string `json:"name,omitempty" yaml:"name,omitempty"` - In string `json:"in,omitempty" yaml:"in,omitempty"` - Scheme string `json:"scheme,omitempty" yaml:"scheme,omitempty"` - BearerFormat string `json:"bearerFormat,omitempty" yaml:"bearerFormat,omitempty"` - Flows *OAuthFlows `json:"flows,omitempty" yaml:"flows,omitempty"` - OpenIdConnectUrl string `json:"openIdConnectUrl,omitempty" yaml:"openIdConnectUrl,omitempty"` + Type string `json:"type,omitempty"` + Description string `json:"description,omitempty"` + Name string `json:"name,omitempty"` + In string `json:"in,omitempty"` + Scheme string `json:"scheme,omitempty"` + BearerFormat string `json:"bearerFormat,omitempty"` + Flows *OAuthFlows `json:"flows,omitempty"` + OpenIdConnectUrl string `json:"openIdConnectUrl,omitempty"` } type SecuritySchemes map[string]SecuritySchemeRef @@ -33,17 +33,17 @@ type SecurityRequirements []SecurityRequirement type SecurityRequirement map[string][]string type OAuthFlows struct { - Implicit *OAuthFlow `json:"implicit,omitempty" yaml:"implicit,omitempty"` - Password *OAuthFlow `json:"password,omitempty" yaml:"password,omitempty"` - ClientCredentials *OAuthFlow `json:"clientCredentials,omitempty" yaml:"clientCredentials,omitempty"` - AuthorizationCode *OAuthFlow `json:"authorizationCode,omitempty" yaml:"authorizationCode,omitempty"` + Implicit *OAuthFlow `json:"implicit,omitempty"` + Password *OAuthFlow `json:"password,omitempty"` + ClientCredentials *OAuthFlow `json:"clientCredentials,omitempty"` + AuthorizationCode *OAuthFlow `json:"authorizationCode,omitempty"` } type OAuthFlow struct { - AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` - TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` - RefreshURL string `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"` - Scopes map[string]string `json:"scopes" yaml:"scopes"` + AuthorizationURL string `json:"authorizationUrl,omitempty"` + TokenURL string `json:"tokenUrl,omitempty"` + RefreshURL string `json:"refreshUrl,omitempty"` + Scopes map[string]string `json:"scopes"` } func (r SecuritySchemeRef) MarshalJSON() ([]byte, error) { diff --git a/protocol/goai/goai_server.go b/protocol/goai/goai_server.go index 3a2d0a07a..7548c8d4a 100644 --- a/protocol/goai/goai_server.go +++ b/protocol/goai/goai_server.go @@ -8,16 +8,16 @@ package goai // Server is specified by OpenAPI/Swagger standard version 3.0. type Server struct { - URL string `json:"url" yaml:"url"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Variables map[string]*ServerVariable `json:"variables,omitempty" yaml:"variables,omitempty"` + URL string `json:"url"` + Description string `json:"description,omitempty"` + Variables map[string]*ServerVariable `json:"variables,omitempty"` } // ServerVariable is specified by OpenAPI/Swagger standard version 3.0. type ServerVariable struct { - Enum []string `json:"enum,omitempty" yaml:"enum,omitempty"` - Default string `json:"default,omitempty" yaml:"default,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` + Enum []string `json:"enum,omitempty"` + Default string `json:"default,omitempty"` + Description string `json:"description,omitempty"` } // Servers is specified by OpenAPI/Swagger standard version 3.0. diff --git a/protocol/goai/goai_shema.go b/protocol/goai/goai_shema.go index ada8c6e9c..5f96628f7 100644 --- a/protocol/goai/goai_shema.go +++ b/protocol/goai/goai_shema.go @@ -9,61 +9,89 @@ package goai import ( "reflect" + "github.com/gogf/gf/v2/container/gmap" + "github.com/gogf/gf/v2/container/gset" "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/internal/json" + "github.com/gogf/gf/v2/internal/utils" "github.com/gogf/gf/v2/os/gstructs" "github.com/gogf/gf/v2/text/gstr" "github.com/gogf/gf/v2/util/gconv" "github.com/gogf/gf/v2/util/gmeta" + "github.com/gogf/gf/v2/util/gvalid" ) -type Schemas map[string]SchemaRef - // Schema is specified by OpenAPI/Swagger 3.0 standard. type Schema struct { - OneOf SchemaRefs `json:"oneOf,omitempty" yaml:"oneOf,omitempty"` - AnyOf SchemaRefs `json:"anyOf,omitempty" yaml:"anyOf,omitempty"` - AllOf SchemaRefs `json:"allOf,omitempty" yaml:"allOf,omitempty"` - Not *SchemaRef `json:"not,omitempty" yaml:"not,omitempty"` - Type string `json:"type,omitempty" yaml:"type,omitempty"` - Title string `json:"title,omitempty" yaml:"title,omitempty"` - Format string `json:"format,omitempty" yaml:"format,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Enum []interface{} `json:"enum,omitempty" yaml:"enum,omitempty"` - Default interface{} `json:"default,omitempty" yaml:"default,omitempty"` - Example interface{} `json:"example,omitempty" yaml:"example,omitempty"` - ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` - UniqueItems bool `json:"uniqueItems,omitempty" yaml:"uniqueItems,omitempty"` - ExclusiveMin bool `json:"exclusiveMinimum,omitempty" yaml:"exclusiveMinimum,omitempty"` - ExclusiveMax bool `json:"exclusiveMaximum,omitempty" yaml:"exclusiveMaximum,omitempty"` - Nullable bool `json:"nullable,omitempty" yaml:"nullable,omitempty"` - ReadOnly bool `json:"readOnly,omitempty" yaml:"readOnly,omitempty"` - WriteOnly bool `json:"writeOnly,omitempty" yaml:"writeOnly,omitempty"` - AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` - XML interface{} `json:"xml,omitempty" yaml:"xml,omitempty"` - Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` - Min *float64 `json:"minimum,omitempty" yaml:"minimum,omitempty"` - Max *float64 `json:"maximum,omitempty" yaml:"maximum,omitempty"` - MultipleOf *float64 `json:"multipleOf,omitempty" yaml:"multipleOf,omitempty"` - MinLength uint64 `json:"minLength,omitempty" yaml:"minLength,omitempty"` - MaxLength *uint64 `json:"maxLength,omitempty" yaml:"maxLength,omitempty"` - Pattern string `json:"pattern,omitempty" yaml:"pattern,omitempty"` - MinItems uint64 `json:"minItems,omitempty" yaml:"minItems,omitempty"` - MaxItems *uint64 `json:"maxItems,omitempty" yaml:"maxItems,omitempty"` - Items *SchemaRef `json:"items,omitempty" yaml:"items,omitempty"` - Required []string `json:"required,omitempty" yaml:"required,omitempty"` - Properties Schemas `json:"properties,omitempty" yaml:"properties,omitempty"` - MinProps uint64 `json:"minProperties,omitempty" yaml:"minProperties,omitempty"` - MaxProps *uint64 `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"` - AdditionalProperties *SchemaRef `json:"additionalProperties,omitempty" yaml:"additionalProperties"` - Discriminator *Discriminator `json:"discriminator,omitempty" yaml:"discriminator,omitempty"` + OneOf SchemaRefs `json:"oneOf,omitempty"` + AnyOf SchemaRefs `json:"anyOf,omitempty"` + AllOf SchemaRefs `json:"allOf,omitempty"` + Not *SchemaRef `json:"not,omitempty"` + Type string `json:"type,omitempty"` + Title string `json:"title,omitempty"` + Format string `json:"format,omitempty"` + Description string `json:"description,omitempty"` + Enum []interface{} `json:"enum,omitempty"` + Default interface{} `json:"default,omitempty"` + Example interface{} `json:"example,omitempty"` + ExternalDocs *ExternalDocs `json:"externalDocs,omitempty"` + UniqueItems bool `json:"uniqueItems,omitempty"` + ExclusiveMin bool `json:"exclusiveMinimum,omitempty"` + ExclusiveMax bool `json:"exclusiveMaximum,omitempty"` + Nullable bool `json:"nullable,omitempty"` + ReadOnly bool `json:"readOnly,omitempty"` + WriteOnly bool `json:"writeOnly,omitempty"` + AllowEmptyValue bool `json:"allowEmptyValue,omitempty"` + XML interface{} `json:"xml,omitempty"` + Deprecated bool `json:"deprecated,omitempty"` + Min *float64 `json:"minimum,omitempty"` + Max *float64 `json:"maximum,omitempty"` + MultipleOf *float64 `json:"multipleOf,omitempty"` + MinLength uint64 `json:"minLength,omitempty"` + MaxLength *uint64 `json:"maxLength,omitempty"` + Pattern string `json:"pattern,omitempty"` + MinItems uint64 `json:"minItems,omitempty"` + MaxItems *uint64 `json:"maxItems,omitempty"` + Items *SchemaRef `json:"items,omitempty"` + Required []string `json:"required,omitempty"` + Properties Schemas `json:"properties,omitempty"` + MinProps uint64 `json:"minProperties,omitempty"` + MaxProps *uint64 `json:"maxProperties,omitempty"` + AdditionalProperties *SchemaRef `json:"additionalProperties,omitempty"` + Discriminator *Discriminator `json:"discriminator,omitempty"` + XExtensions XExtensions `json:"-"` +} + +func (s Schema) MarshalJSON() ([]byte, error) { + var ( + b []byte + m map[string]json.RawMessage + err error + ) + type tempSchema Schema // To prevent JSON marshal recursion error. + if b, err = json.Marshal(tempSchema(s)); err != nil { + return nil, err + } + if err = json.Unmarshal(b, &m); err != nil { + return nil, err + } + for k, v := range s.XExtensions { + if b, err = json.Marshal(v); err != nil { + return nil, err + } + m[k] = b + } + return json.Marshal(m) } // Discriminator is specified by OpenAPI/Swagger standard version 3.0. type Discriminator struct { - PropertyName string `json:"propertyName" yaml:"propertyName"` - Mapping map[string]string `json:"mapping,omitempty" yaml:"mapping,omitempty"` + PropertyName string `json:"propertyName"` + Mapping map[string]string `json:"mapping,omitempty"` } +// addSchema creates schemas with objects. +// Note that the `object` can be array alias like: `type Res []Item`. func (oai *OpenApiV3) addSchema(object ...interface{}) error { for _, v := range object { if err := oai.doAddSchemaSingle(v); err != nil { @@ -74,8 +102,8 @@ func (oai *OpenApiV3) addSchema(object ...interface{}) error { } func (oai *OpenApiV3) doAddSchemaSingle(object interface{}) error { - if oai.Components.Schemas == nil { - oai.Components.Schemas = map[string]SchemaRef{} + if oai.Components.Schemas.refs == nil { + oai.Components.Schemas.refs = gmap.NewListMap() } var ( @@ -84,53 +112,63 @@ func (oai *OpenApiV3) doAddSchemaSingle(object interface{}) error { ) // Already added. - if _, ok := oai.Components.Schemas[structTypeName]; ok { + if oai.Components.Schemas.Get(structTypeName) != nil { return nil } // Take the holder first. - oai.Components.Schemas[structTypeName] = SchemaRef{} + oai.Components.Schemas.Set(structTypeName, SchemaRef{}) schema, err := oai.structToSchema(object) if err != nil { return err } - oai.Components.Schemas[structTypeName] = SchemaRef{ + oai.Components.Schemas.Set(structTypeName, SchemaRef{ Ref: "", Value: schema, - } + }) return nil } // structToSchema converts and returns given struct object as Schema. func (oai *OpenApiV3) structToSchema(object interface{}) (*Schema, error) { - structFields, _ := gstructs.Fields(gstructs.FieldsInput{ - Pointer: object, - RecursiveOption: gstructs.RecursiveOptionEmbeddedNoTag, - }) var ( tagMap = gmeta.Data(object) schema = &Schema{ - Properties: map[string]SchemaRef{}, + Properties: createSchemas(), + XExtensions: make(XExtensions), } + ignoreProperties []interface{} ) if len(tagMap) > 0 { - err := gconv.Struct(oai.fileMapWithShortTags(tagMap), schema) - if err != nil { - return nil, gerror.Wrap(err, `mapping meta data tags to Schema failed`) + if err := oai.tagMapToSchema(tagMap, schema); err != nil { + return nil, err } } if schema.Type != "" && schema.Type != TypeObject { return schema, nil } + // []struct. + if utils.IsArray(object) { + schema.Type = TypeArray + subSchemaRef, err := oai.newSchemaRefWithGolangType(reflect.TypeOf(object).Elem(), nil) + if err != nil { + return nil, err + } + schema.Items = subSchemaRef + return schema, nil + } + // struct. + structFields, _ := gstructs.Fields(gstructs.FieldsInput{ + Pointer: object, + RecursiveOption: gstructs.RecursiveOptionEmbeddedNoTag, + }) schema.Type = TypeObject for _, structField := range structFields { if !gstr.IsLetterUpper(structField.Name()[0]) { continue } - var ( - fieldName = structField.Name() - ) + var fieldName = structField.Name() if jsonName := structField.TagJsonName(); jsonName != "" { fieldName = jsonName } @@ -141,7 +179,64 @@ func (oai *OpenApiV3) structToSchema(object interface{}) (*Schema, error) { if err != nil { return nil, err } - schema.Properties[fieldName] = *schemaRef + schema.Properties.Set(fieldName, *schemaRef) } + + schema.Properties.Iterator(func(key string, ref SchemaRef) bool { + if ref.Value != nil && ref.Value.Pattern != "" { + validationRuleSet := gset.NewStrSetFrom(gstr.Split(ref.Value.Pattern, "|")) + if validationRuleSet.Contains(patternKeyForRequired) { + schema.Required = append(schema.Required, key) + } + } + if !isValidParameterName(key) { + ignoreProperties = append(ignoreProperties, key) + } + return true + }) + + if len(ignoreProperties) > 0 { + schema.Properties.Removes(ignoreProperties) + } + return schema, nil } + +func (oai *OpenApiV3) tagMapToSchema(tagMap map[string]string, schema *Schema) error { + var mergedTagMap = oai.fileMapWithShortTags(tagMap) + if err := gconv.Struct(mergedTagMap, schema); err != nil { + return gerror.Wrap(err, `mapping struct tags to Schema failed`) + } + oai.tagMapToXExtensions(mergedTagMap, schema.XExtensions) + // Validation info to OpenAPI schema pattern. + for _, tag := range gvalid.GetTags() { + if validationTagValue, ok := tagMap[tag]; ok { + _, validationRules, _ := gvalid.ParseTagValue(validationTagValue) + schema.Pattern = validationRules + // Enum checks. + if len(schema.Enum) == 0 { + for _, rule := range gstr.SplitAndTrim(validationRules, "|") { + if gstr.HasPrefix(rule, patternKeyForIn) { + var ( + isAllEnumNumber = true + enumArray = gstr.SplitAndTrim(rule[len(patternKeyForIn):], ",") + ) + for _, enum := range enumArray { + if !gstr.IsNumeric(enum) { + isAllEnumNumber = false + break + } + } + if isAllEnumNumber { + schema.Enum = gconv.Interfaces(gconv.Int64s(enumArray)) + } else { + schema.Enum = gconv.Interfaces(enumArray) + } + } + } + } + break + } + } + return nil +} diff --git a/protocol/goai/goai_shemaref.go b/protocol/goai/goai_shema_ref.go similarity index 81% rename from protocol/goai/goai_shemaref.go rename to protocol/goai/goai_shema_ref.go index 14d356686..7480b8e67 100644 --- a/protocol/goai/goai_shemaref.go +++ b/protocol/goai/goai_shema_ref.go @@ -9,9 +9,7 @@ package goai import ( "reflect" - "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/internal/json" - "github.com/gogf/gf/v2/util/gconv" ) type SchemaRefs []SchemaRef @@ -27,18 +25,20 @@ func (oai *OpenApiV3) newSchemaRefWithGolangType(golangType reflect.Type, tagMap oaiFormat = oai.golangTypeToOAIFormat(golangType) schemaRef = &SchemaRef{} schema = &Schema{ - Type: oaiType, - Format: oaiFormat, + Type: oaiType, + Format: oaiFormat, + XExtensions: make(XExtensions), } ) if len(tagMap) > 0 { - if err := gconv.Struct(oai.fileMapWithShortTags(tagMap), schema); err != nil { - return nil, gerror.Wrap(err, `mapping struct tags to Schema failed`) + if err := oai.tagMapToSchema(tagMap, schema); err != nil { + return nil, err } } schemaRef.Value = schema switch oaiType { case + TypeInteger, TypeNumber, TypeString, TypeBoolean: @@ -72,7 +72,7 @@ func (oai *OpenApiV3) newSchemaRefWithGolangType(golangType reflect.Type, tagMap var ( structTypeName = oai.golangTypeToSchemaName(golangType) ) - if _, ok := oai.Components.Schemas[structTypeName]; !ok { + if oai.Components.Schemas.Get(structTypeName) == nil { if err := oai.addSchema(reflect.New(golangType).Interface()); err != nil { return nil, err } @@ -82,10 +82,8 @@ func (oai *OpenApiV3) newSchemaRefWithGolangType(golangType reflect.Type, tagMap default: // Normal struct object. - var ( - structTypeName = oai.golangTypeToSchemaName(golangType) - ) - if _, ok := oai.Components.Schemas[structTypeName]; !ok { + var structTypeName = oai.golangTypeToSchemaName(golangType) + if oai.Components.Schemas.Get(structTypeName) == nil { if err := oai.addSchema(reflect.New(golangType).Elem().Interface()); err != nil { return nil, err } diff --git a/protocol/goai/goai_shemas.go b/protocol/goai/goai_shemas.go new file mode 100644 index 000000000..c8f6c6458 --- /dev/null +++ b/protocol/goai/goai_shemas.go @@ -0,0 +1,69 @@ +// Copyright GoFrame Author(https://goframe.org). 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 goai + +import ( + "github.com/gogf/gf/v2/container/gmap" +) + +type Schemas struct { + refs *gmap.ListMap // map[string]SchemaRef +} + +func createSchemas() Schemas { + return Schemas{ + refs: gmap.NewListMap(), + } +} + +func (s *Schemas) init() { + if s.refs == nil { + s.refs = gmap.NewListMap() + } +} + +func (s *Schemas) Get(name string) *SchemaRef { + s.init() + value := s.refs.Get(name) + if value != nil { + ref := value.(SchemaRef) + return &ref + } + return nil +} + +func (s *Schemas) Set(name string, ref SchemaRef) { + s.init() + s.refs.Set(name, ref) +} + +func (s *Schemas) Removes(names []interface{}) { + s.init() + s.refs.Removes(names) +} + +func (s *Schemas) Map() map[string]SchemaRef { + s.init() + m := make(map[string]SchemaRef) + s.refs.Iterator(func(key, value interface{}) bool { + m[key.(string)] = value.(SchemaRef) + return true + }) + return m +} + +func (s *Schemas) Iterator(f func(key string, ref SchemaRef) bool) { + s.init() + s.refs.Iterator(func(key, value interface{}) bool { + return f(key.(string), value.(SchemaRef)) + }) +} + +func (s Schemas) MarshalJSON() ([]byte, error) { + s.init() + return s.refs.MarshalJSON() +} diff --git a/protocol/goai/goai_tags.go b/protocol/goai/goai_tags.go index 844d9a634..6d14ca6ed 100644 --- a/protocol/goai/goai_tags.go +++ b/protocol/goai/goai_tags.go @@ -11,7 +11,7 @@ type Tags []Tag // Tag is specified by OpenAPI/Swagger 3.0 standard. type Tag struct { - Name string `json:"name,omitempty" yaml:"name,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + ExternalDocs *ExternalDocs `json:"externalDocs,omitempty"` } diff --git a/protocol/goai/goai_xextensions.go b/protocol/goai/goai_xextensions.go new file mode 100644 index 000000000..cc11ef6a6 --- /dev/null +++ b/protocol/goai/goai_xextensions.go @@ -0,0 +1,22 @@ +// Copyright GoFrame Author(https://goframe.org). 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 goai + +import ( + "github.com/gogf/gf/v2/text/gstr" +) + +// XExtensions stores the `x-` custom extensions. +type XExtensions map[string]interface{} + +func (oai *OpenApiV3) tagMapToXExtensions(tagMap map[string]string, extensions XExtensions) { + for k, v := range tagMap { + if gstr.HasPrefix(k, "x-") || gstr.HasPrefix(k, "X-") { + extensions[k] = v + } + } +} diff --git a/protocol/goai/goai_z_unit_test.go b/protocol/goai/goai_z_unit_test.go index 9acda5b21..e81b04715 100644 --- a/protocol/goai/goai_z_unit_test.go +++ b/protocol/goai/goai_z_unit_test.go @@ -48,14 +48,14 @@ func Test_Basic(t *testing.T) { }) t.AssertNil(err) // Schema asserts. - t.Assert(len(oai.Components.Schemas), 2) - t.Assert(oai.Components.Schemas[`github.com.gogf.gf.v2.protocol.goai_test.CreateResourceReq`].Value.Type, goai.TypeObject) - t.Assert(len(oai.Components.Schemas[`github.com.gogf.gf.v2.protocol.goai_test.CreateResourceReq`].Value.Properties), 7) - t.Assert(oai.Components.Schemas[`github.com.gogf.gf.v2.protocol.goai_test.CreateResourceReq`].Value.Properties[`appId`].Value.Type, goai.TypeNumber) - t.Assert(oai.Components.Schemas[`github.com.gogf.gf.v2.protocol.goai_test.CreateResourceReq`].Value.Properties[`resourceId`].Value.Type, goai.TypeString) + t.Assert(len(oai.Components.Schemas.Map()), 2) + t.Assert(oai.Components.Schemas.Get(`github.com.gogf.gf.v2.protocol.goai_test.CreateResourceReq`).Value.Type, goai.TypeObject) + t.Assert(len(oai.Components.Schemas.Get(`github.com.gogf.gf.v2.protocol.goai_test.CreateResourceReq`).Value.Properties.Map()), 7) + t.Assert(oai.Components.Schemas.Get(`github.com.gogf.gf.v2.protocol.goai_test.CreateResourceReq`).Value.Properties.Get(`appId`).Value.Type, goai.TypeInteger) + t.Assert(oai.Components.Schemas.Get(`github.com.gogf.gf.v2.protocol.goai_test.CreateResourceReq`).Value.Properties.Get(`resourceId`).Value.Type, goai.TypeString) - t.Assert(len(oai.Components.Schemas[`github.com.gogf.gf.v2.protocol.goai_test.SetSpecInfo`].Value.Properties), 3) - t.Assert(oai.Components.Schemas[`github.com.gogf.gf.v2.protocol.goai_test.SetSpecInfo`].Value.Properties[`Params`].Value.Type, goai.TypeArray) + t.Assert(len(oai.Components.Schemas.Get(`github.com.gogf.gf.v2.protocol.goai_test.SetSpecInfo`).Value.Properties.Map()), 3) + t.Assert(oai.Components.Schemas.Get(`github.com.gogf.gf.v2.protocol.goai_test.SetSpecInfo`).Value.Properties.Get(`Params`).Value.Type, goai.TypeArray) }) } @@ -108,14 +108,14 @@ func TestOpenApiV3_Add(t *testing.T) { t.AssertNil(err) // fmt.Println(oai.String()) // Schema asserts. - t.Assert(len(oai.Components.Schemas), 3) - t.Assert(oai.Components.Schemas[`github.com.gogf.gf.v2.protocol.goai_test.CreateResourceReq`].Value.Type, goai.TypeObject) - t.Assert(len(oai.Components.Schemas[`github.com.gogf.gf.v2.protocol.goai_test.CreateResourceReq`].Value.Properties), 7) - t.Assert(oai.Components.Schemas[`github.com.gogf.gf.v2.protocol.goai_test.CreateResourceReq`].Value.Properties[`appId`].Value.Type, goai.TypeNumber) - t.Assert(oai.Components.Schemas[`github.com.gogf.gf.v2.protocol.goai_test.CreateResourceReq`].Value.Properties[`resourceId`].Value.Type, goai.TypeString) + t.Assert(len(oai.Components.Schemas.Map()), 3) + t.Assert(oai.Components.Schemas.Get(`github.com.gogf.gf.v2.protocol.goai_test.CreateResourceReq`).Value.Type, goai.TypeObject) + t.Assert(len(oai.Components.Schemas.Get(`github.com.gogf.gf.v2.protocol.goai_test.CreateResourceReq`).Value.Properties.Map()), 7) + t.Assert(oai.Components.Schemas.Get(`github.com.gogf.gf.v2.protocol.goai_test.CreateResourceReq`).Value.Properties.Get(`appId`).Value.Type, goai.TypeInteger) + t.Assert(oai.Components.Schemas.Get(`github.com.gogf.gf.v2.protocol.goai_test.CreateResourceReq`).Value.Properties.Get(`resourceId`).Value.Type, goai.TypeString) - t.Assert(len(oai.Components.Schemas[`github.com.gogf.gf.v2.protocol.goai_test.SetSpecInfo`].Value.Properties), 3) - t.Assert(oai.Components.Schemas[`github.com.gogf.gf.v2.protocol.goai_test.SetSpecInfo`].Value.Properties[`Params`].Value.Type, goai.TypeArray) + t.Assert(len(oai.Components.Schemas.Get(`github.com.gogf.gf.v2.protocol.goai_test.SetSpecInfo`).Value.Properties.Map()), 3) + t.Assert(oai.Components.Schemas.Get(`github.com.gogf.gf.v2.protocol.goai_test.SetSpecInfo`).Value.Properties.Get(`Params`).Value.Type, goai.TypeArray) // Paths. t.Assert(len(oai.Paths), 1) @@ -158,9 +158,9 @@ func TestOpenApiV3_Add_Recursive(t *testing.T) { }) t.AssertNil(err) // Schema asserts. - t.Assert(len(oai.Components.Schemas), 3) - t.Assert(oai.Components.Schemas[`github.com.gogf.gf.v2.protocol.goai_test.CategoryTreeItem`].Value.Type, goai.TypeObject) - t.Assert(len(oai.Components.Schemas[`github.com.gogf.gf.v2.protocol.goai_test.CategoryTreeItem`].Value.Properties), 3) + t.Assert(len(oai.Components.Schemas.Map()), 3) + t.Assert(oai.Components.Schemas.Get(`github.com.gogf.gf.v2.protocol.goai_test.CategoryTreeItem`).Value.Type, goai.TypeObject) + t.Assert(len(oai.Components.Schemas.Get(`github.com.gogf.gf.v2.protocol.goai_test.CategoryTreeItem`).Value.Properties.Map()), 3) }) } @@ -222,7 +222,7 @@ func TestOpenApiV3_Add_AutoDetectIn(t *testing.T) { fmt.Println(oai.String()) - t.Assert(len(oai.Components.Schemas), 2) + t.Assert(len(oai.Components.Schemas.Map()), 2) t.Assert(len(oai.Paths), 1) t.AssertNE(oai.Paths[path].Get, nil) t.Assert(len(oai.Paths[path].Get.Parameters), 3) @@ -274,9 +274,9 @@ func TestOpenApiV3_CommonRequest(t *testing.T) { }) t.AssertNil(err) // Schema asserts. - t.Assert(len(oai.Components.Schemas), 3) + t.Assert(len(oai.Components.Schemas.Map()), 3) t.Assert(len(oai.Paths), 1) - t.Assert(len(oai.Paths["/index"].Put.RequestBody.Value.Content["application/json"].Schema.Value.Properties), 3) + t.Assert(len(oai.Paths["/index"].Put.RequestBody.Value.Content["application/json"].Schema.Value.Properties.Map()), 3) }) } @@ -319,9 +319,9 @@ func TestOpenApiV3_CommonRequest_WithoutDataField_Setting(t *testing.T) { t.AssertNil(err) // Schema asserts. // fmt.Println(oai.String()) - t.Assert(len(oai.Components.Schemas), 3) + t.Assert(len(oai.Components.Schemas.Map()), 3) t.Assert(len(oai.Paths), 1) - t.Assert(len(oai.Paths["/index"].Put.RequestBody.Value.Content["application/json"].Schema.Value.Properties), 5) + t.Assert(len(oai.Paths["/index"].Put.RequestBody.Value.Content["application/json"].Schema.Value.Properties.Map()), 5) }) } @@ -359,9 +359,9 @@ func TestOpenApiV3_CommonRequest_EmptyRequest(t *testing.T) { t.AssertNil(err) // Schema asserts. // fmt.Println(oai.String()) - t.Assert(len(oai.Components.Schemas), 3) + t.Assert(len(oai.Components.Schemas.Map()), 3) t.Assert(len(oai.Paths), 1) - t.Assert(len(oai.Paths["/index"].Put.RequestBody.Value.Content["application/json"].Schema.Value.Properties), 3) + t.Assert(len(oai.Paths["/index"].Put.RequestBody.Value.Content["application/json"].Schema.Value.Properties.Map()), 3) }) } @@ -414,10 +414,10 @@ func TestOpenApiV3_CommonRequest_SubDataField(t *testing.T) { t.AssertNil(err) // Schema asserts. // fmt.Println(oai.String()) - t.Assert(len(oai.Components.Schemas), 4) + t.Assert(len(oai.Components.Schemas.Map()), 4) t.Assert(len(oai.Paths), 1) - t.Assert(len(oai.Paths["/index"].Put.RequestBody.Value.Content["application/json"].Schema.Value.Properties), 1) - t.Assert(len(oai.Paths["/index"].Put.RequestBody.Value.Content["application/json"].Schema.Value.Properties[`Request`].Value.Properties), 4) + t.Assert(len(oai.Paths["/index"].Put.RequestBody.Value.Content["application/json"].Schema.Value.Properties.Map()), 1) + t.Assert(len(oai.Paths["/index"].Put.RequestBody.Value.Content["application/json"].Schema.Value.Properties.Get(`Request`).Value.Properties.Map()), 4) }) } @@ -459,10 +459,16 @@ func TestOpenApiV3_CommonResponse(t *testing.T) { Object: f, }) t.AssertNil(err) + + //g.Dump(oai.Paths["/index"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Properties.Map()) // Schema asserts. - t.Assert(len(oai.Components.Schemas), 3) + t.Assert(len(oai.Components.Schemas.Map()), 3) t.Assert(len(oai.Paths), 1) - t.Assert(len(oai.Paths["/index"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Properties), 3) + t.Assert(len(oai.Paths["/index"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Properties.Map()), 3) + t.Assert( + oai.Paths["/index"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Properties.Get("data").Value.Description, + `Result data for certain request according API definition`, + ) }) } @@ -505,9 +511,9 @@ func TestOpenApiV3_CommonResponse_WithoutDataField_Setting(t *testing.T) { t.AssertNil(err) // Schema asserts. fmt.Println(oai.String()) - t.Assert(len(oai.Components.Schemas), 3) + t.Assert(len(oai.Components.Schemas.Map()), 3) t.Assert(len(oai.Paths), 1) - t.Assert(len(oai.Paths["/index"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Properties), 8) + t.Assert(len(oai.Paths["/index"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Properties.Map()), 8) }) } @@ -545,10 +551,10 @@ func TestOpenApiV3_CommonResponse_EmptyResponse(t *testing.T) { t.AssertNil(err) // Schema asserts. // fmt.Println(oai.String()) - t.Assert(len(oai.Components.Schemas), 3) + t.Assert(len(oai.Components.Schemas.Map()), 3) t.Assert(len(oai.Paths), 1) t.Assert(oai.Paths["/index"].Put.RequestBody.Value.Content["application/json"].Schema.Ref, `github.com.gogf.gf.v2.protocol.goai_test.Req`) - t.Assert(len(oai.Paths["/index"].Put.Responses["200"].Value.Content["application/json"].Schema.Value.Properties), 3) + t.Assert(len(oai.Paths["/index"].Put.Responses["200"].Value.Content["application/json"].Schema.Value.Properties.Map()), 3) }) } @@ -601,10 +607,10 @@ func TestOpenApiV3_CommonResponse_SubDataField(t *testing.T) { t.AssertNil(err) // Schema asserts. // fmt.Println(oai.String()) - t.Assert(len(oai.Components.Schemas), 4) + t.Assert(len(oai.Components.Schemas.Map()), 4) t.Assert(len(oai.Paths), 1) - t.Assert(len(oai.Paths["/index"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Properties), 1) - t.Assert(len(oai.Paths["/index"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Properties[`Response`].Value.Properties), 7) + t.Assert(len(oai.Paths["/index"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Properties.Map()), 1) + t.Assert(len(oai.Paths["/index"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Properties.Get(`Response`).Value.Properties.Map()), 7) }) } @@ -657,12 +663,12 @@ func TestOpenApiV3_ShortTags(t *testing.T) { t.AssertNil(err) // fmt.Println(oai.String()) // Schema asserts. - t.Assert(len(oai.Components.Schemas), 3) + t.Assert(len(oai.Components.Schemas.Map()), 3) t.Assert(oai.Paths[`/test1/{appId}`].Summary, `CreateResourceReq sum`) t.Assert(oai. Components. - Schemas[`github.com.gogf.gf.v2.protocol.goai_test.CreateResourceReq`]. - Value.Properties[`resourceId`].Value.Description, `资源Id`) + Schemas.Get(`github.com.gogf.gf.v2.protocol.goai_test.CreateResourceReq`). + Value.Properties.Get(`resourceId`).Value.Description, `资源Id`) }) } @@ -692,7 +698,7 @@ func TestOpenApiV3_HtmlResponse(t *testing.T) { t.AssertNil(err) // fmt.Println(oai.String()) - t.Assert(oai.Components.Schemas[`github.com.gogf.gf.v2.protocol.goai_test.Res`].Value.Type, goai.TypeString) + t.Assert(oai.Components.Schemas.Get(`github.com.gogf.gf.v2.protocol.goai_test.Res`).Value.Type, goai.TypeString) }) } @@ -739,6 +745,133 @@ func TestOpenApiV3_HtmlResponseWithCommonResponse(t *testing.T) { t.AssertNil(err) // fmt.Println(oai.String()) - t.Assert(oai.Components.Schemas[`github.com.gogf.gf.v2.protocol.goai_test.Res`].Value.Type, goai.TypeString) + t.Assert(oai.Components.Schemas.Get(`github.com.gogf.gf.v2.protocol.goai_test.Res`).Value.Type, goai.TypeString) + }) +} + +func Test_Required_In_Schema(t *testing.T) { + type CommonReq struct { + AppId int64 `json:"appId" v:"required" in:"cookie" description:"应用Id"` + ResourceId string `json:"resourceId" in:"query" description:"资源Id"` + } + type SetSpecInfo struct { + StorageType string `v:"required|in:CLOUD_PREMIUM,CLOUD_SSD,CLOUD_HSSD" description:"StorageType"` + Shards int32 `description:"shards 分片数"` + Params []string `description:"默认参数(json 串-ClickHouseParams)"` + } + type CreateResourceReq struct { + CommonReq + gmeta.Meta `path:"/CreateResourceReq" method:"POST" tags:"default"` + Name string `description:"实例名称"` + Product string `description:"业务类型"` + Region string `v:"required|min:1" description:"区域"` + SetMap map[string]*SetSpecInfo `v:"required|min:1" description:"配置Map"` + SetSlice []SetSpecInfo `v:"required|min:1" description:"配置Slice"` + } + + gtest.C(t, func(t *gtest.T) { + var ( + err error + oai = goai.New() + req = new(CreateResourceReq) + ) + err = oai.Add(goai.AddInput{ + Object: req, + }) + t.AssertNil(err) + var ( + schemaKey1 = `github.com.gogf.gf.v2.protocol.goai_test.CreateResourceReq` + schemaKey2 = `github.com.gogf.gf.v2.protocol.goai_test.SetSpecInfo` + ) + t.Assert(oai.Components.Schemas.Map()[schemaKey1].Value.Required, g.Slice{ + "appId", + "Region", + "SetMap", + "SetSlice", + }) + t.Assert(oai.Components.Schemas.Map()[schemaKey2].Value.Required, g.Slice{ + "StorageType", + }) + t.Assert(oai.Components.Schemas.Map()[schemaKey2].Value.Properties.Map()["StorageType"].Value.Enum, g.Slice{ + "CLOUD_PREMIUM", + "CLOUD_SSD", + "CLOUD_HSSD", + }) + }) +} + +func Test_Properties_In_Sequence(t *testing.T) { + type ResourceCreateReq struct { + g.Meta `path:"/resource" tags:"OSS Resource" method:"put" x-sort:"1" summary:"创建实例(发货)"` + AppId uint64 `v:"required" dc:"应用Id"` + Uin string `v:"required" dc:"主用户账号,该资源隶属于的账号"` + CreateUin string `v:"required" dc:"创建实例的用户账号"` + Product string `v:"required" dc:"业务类型" eg:"tdach"` + Region string `v:"required" dc:"地域" eg:"ap-guangzhou"` + Zone string `v:"required" dc:"区域" eg:"ap-guangzhou-1"` + Tenant string `v:"required" dc:"业务自定义数据,透传到底层"` + VpcId string `dc:"业务Vpc Id, TCS场景下非必须"` + SubnetId string `dc:"业务Vpc子网Id"` + Name string `dc:"自定义实例名称,默认和ResourceId一致"` + ClusterPreset string `dc:"业务自定义Cluster定义,透传到底层"` + Engine string `dc:"引擎名称,例如:TxLightning"` + Version string `dc:"引擎版本,例如:10.3.213 (兼容ClickHouse 21.3.12)"` + SkipUpdateStatus bool `dc:"是否跳过状态更新,继续保持creating" ed:"http://goframe.org"` + } + gtest.C(t, func(t *gtest.T) { + var ( + err error + oai = goai.New() + req = new(ResourceCreateReq) + ) + err = oai.Add(goai.AddInput{ + Object: req, + }) + t.AssertNil(err) + fmt.Println(oai) + }) +} + +func TestOpenApiV3_Ignore_Parameter(t *testing.T) { + type CommonResponse struct { + Code int `json:"code" description:"Error code"` + Message string `json:"message" description:"Error message"` + Data interface{} `json:"data" description:"Result data for certain request according API definition"` + } + type ProductSearchReq struct { + gmeta.Meta `path:"/test" method:"get"` + Product string `json:"-" in:"query" v:"required|max:5" description:"Unique product key"` + Name string `json:"name" in:"query" v:"required" description:"Instance name"` + } + type ProductSearchRes struct { + ID int64 `json:"-" description:"Product ID"` + Product string `json:"product" v:"required" description:"Unique product key"` + TemplateName string `json:"templateName" v:"required" description:"Workflow template name"` + Version string `json:"version" v:"required" description:"Workflow template version"` + TxID string `json:"txID" v:"required" description:"Transaction ID for creating instance"` + Globals string `json:"globals" description:"Globals"` + } + + f := func(ctx context.Context, req *ProductSearchReq) (res *ProductSearchRes, err error) { + return + } + + gtest.C(t, func(t *gtest.T) { + var ( + err error + oai = goai.New() + ) + + oai.Config.CommonResponse = CommonResponse{} + + err = oai.Add(goai.AddInput{ + Object: f, + }) + t.AssertNil(err) + // Schema asserts. + // fmt.Println(oai.String()) + t.Assert(len(oai.Components.Schemas.Map()), 3) + t.Assert(len(oai.Paths), 1) + t.Assert(len(oai.Paths["/test"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Properties.Map()), 8) }) } diff --git a/protocol/goai/testdata/example.yaml b/protocol/goai/testdata/example.yaml index ad3972205..96ba85255 100644 --- a/protocol/goai/testdata/example.yaml +++ b/protocol/goai/testdata/example.yaml @@ -135,7 +135,7 @@ paths: tags: - pet summary: Finds Pets by tags - description: Muliple tags can be provided with comma separated strings. Use tag1, + description: Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. operationId: findPetsByTags parameters: diff --git a/test/gtest/gtest_util.go b/test/gtest/gtest_util.go index d8fa33d1d..ca3896d36 100644 --- a/test/gtest/gtest_util.go +++ b/test/gtest/gtest_util.go @@ -29,7 +29,7 @@ const ( func C(t *testing.T, f func(t *T)) { defer func() { if err := recover(); err != nil { - fmt.Fprintf(os.Stderr, "%v\n%s", err, gdebug.StackWithFilter([]string{pathFilterKey})) + _, _ = fmt.Fprintf(os.Stderr, "%v\n%s", err, gdebug.StackWithFilter([]string{pathFilterKey})) t.Fail() } }() @@ -342,14 +342,14 @@ func AssertNil(value interface{}) { if err, ok := value.(error); ok { panic(fmt.Sprintf(`%+v`, err)) } - AssertNE(value, nil) + Assert(value, nil) } -// TestDataPath retrieves and returns the testdata path of current package, +// DataPath retrieves and returns the testdata path of current package, // which is used for unit testing cases only. // The optional parameter `names` specifies the sub-folders/sub-files, // which will be joined with current system separator and returned with the path. -func TestDataPath(names ...string) string { +func DataPath(names ...string) string { _, path, _ := gdebug.CallerWithFilter([]string{pathFilterKey}) path = filepath.Dir(path) + string(filepath.Separator) + "testdata" for _, name := range names { @@ -358,9 +358,9 @@ func TestDataPath(names ...string) string { return path } -// TestDataContent retrieves and returns the file content for specified testdata path of current package -func TestDataContent(names ...string) string { - path := TestDataPath(names...) +// DataContent retrieves and returns the file content for specified testdata path of current package +func DataContent(names ...string) string { + path := DataPath(names...) if path != "" { data, err := ioutil.ReadFile(path) if err == nil { diff --git a/text/gregex/gregex_cache.go b/text/gregex/gregex_cache.go index 7ceb778e4..1cc9099c0 100644 --- a/text/gregex/gregex_cache.go +++ b/text/gregex/gregex_cache.go @@ -38,8 +38,7 @@ func getRegexp(pattern string) (regex *regexp.Regexp, err error) { } // If it does not exist in the cache, // it compiles the pattern and creates one. - regex, err = regexp.Compile(pattern) - if err != nil { + if regex, err = regexp.Compile(pattern); err != nil { err = gerror.Wrapf(err, `regexp.Compile failed for pattern "%s"`, pattern) return } diff --git a/text/gregex/gregex_z_unit_test.go b/text/gregex/gregex_z_unit_test.go index bf9aae5ab..ca07f8407 100644 --- a/text/gregex/gregex_z_unit_test.go +++ b/text/gregex/gregex_z_unit_test.go @@ -70,7 +70,7 @@ func Test_Match(t *testing.T) { wantSubs := "aaabb" s := "acbb" + wantSubs + "dd" subs, err := gregex.Match(re, []byte(s)) - t.Assert(err, nil) + t.AssertNil(err) if string(subs[0]) != wantSubs { t.Fatalf("regex:%s,Match(%q)[0] = %q; want %q", re, s, subs[0], wantSubs) } @@ -89,7 +89,7 @@ func Test_MatchString(t *testing.T) { wantSubs := "aaabb" s := "acbb" + wantSubs + "dd" subs, err := gregex.MatchString(re, s) - t.Assert(err, nil) + t.AssertNil(err) if string(subs[0]) != wantSubs { t.Fatalf("regex:%s,Match(%q)[0] = %q; want %q", re, s, subs[0], wantSubs) } @@ -109,7 +109,7 @@ func Test_MatchAll(t *testing.T) { s := "acbb" + wantSubs + "dd" s = s + `其他的` + s subs, err := gregex.MatchAll(re, []byte(s)) - t.Assert(err, nil) + t.AssertNil(err) if string(subs[0][0]) != wantSubs { t.Fatalf("regex:%s,Match(%q)[0] = %q; want %q", re, s, subs[0][0], wantSubs) } @@ -135,7 +135,7 @@ func Test_MatchAllString(t *testing.T) { wantSubs := "aaabb" s := "acbb" + wantSubs + "dd" subs, err := gregex.MatchAllString(re, s+`其他的`+s) - t.Assert(err, nil) + t.AssertNil(err) if string(subs[0][0]) != wantSubs { t.Fatalf("regex:%s,Match(%q)[0] = %q; want %q", re, s, subs[0][0], wantSubs) } @@ -163,7 +163,7 @@ func Test_Replace(t *testing.T) { s := "acbb" + wantSubs + "dd" wanted := "acbb" + replace + "dd" replacedStr, err := gregex.Replace(re, []byte(replace), []byte(s)) - t.Assert(err, nil) + t.AssertNil(err) if string(replacedStr) != wanted { t.Fatalf("regex:%s,old:%s; want %q", re, s, wanted) } @@ -181,7 +181,7 @@ func Test_ReplaceString(t *testing.T) { s := "acbb" + wantSubs + "dd" wanted := "acbb" + replace + "dd" replacedStr, err := gregex.ReplaceString(re, replace, s) - t.Assert(err, nil) + t.AssertNil(err) if replacedStr != wanted { t.Fatalf("regex:%s,old:%s; want %q", re, s, wanted) } @@ -205,7 +205,7 @@ func Test_ReplaceFun(t *testing.T) { } return []byte("[x" + string(s) + "y]") }) - t.Assert(err, nil) + t.AssertNil(err) if string(replacedStr) != wanted { t.Fatalf("regex:%s,old:%s; want %q", re, s, wanted) } @@ -263,7 +263,7 @@ func Test_ReplaceStringFunc(t *testing.T) { } return "[x" + s + "y]" }) - t.Assert(err, nil) + t.AssertNil(err) if replacedStr != wanted { t.Fatalf("regex:%s,old:%s; want %q", re, s, wanted) } diff --git a/text/gstr/gstr_replace.go b/text/gstr/gstr_replace.go index 713fc02c7..1449f9be0 100644 --- a/text/gstr/gstr_replace.go +++ b/text/gstr/gstr_replace.go @@ -33,21 +33,17 @@ func ReplaceI(origin, search, replace string, count ...int) string { return origin } var ( - searchLength = len(search) - searchLower = strings.ToLower(search) - originLower string - pos int - diff = len(replace) - searchLength + searchLength = len(search) + replaceLength = len(replace) + searchLower = strings.ToLower(search) + originLower string + pos int ) for { originLower = strings.ToLower(origin) if pos = Pos(originLower, searchLower, pos); pos != -1 { origin = origin[:pos] + replace + origin[pos+searchLength:] - if diff < 0 { - pos += -diff - } else { - pos += diff + 1 - } + pos += replaceLength if n--; n == 0 { break } diff --git a/text/gstr/gstr_z_unit_parse_test.go b/text/gstr/gstr_z_unit_parse_test.go index 1dbed1ac5..9d4bd90cd 100644 --- a/text/gstr/gstr_z_unit_parse_test.go +++ b/text/gstr/gstr_z_unit_parse_test.go @@ -22,41 +22,41 @@ func Test_Parse(t *testing.T) { gtest.C(t, func(t *gtest.T) { s := "goframe.org/index?name=john&score=100" u, err := url.Parse(s) - t.Assert(err, nil) + t.AssertNil(err) m, err := gstr.Parse(u.RawQuery) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(m["name"], "john") t.Assert(m["score"], "100") // name overwrite m, err = gstr.Parse("a=1&a=2") - t.Assert(err, nil) + t.AssertNil(err) t.Assert(m, g.Map{ "a": 2, }) // slice m, err = gstr.Parse("a[]=1&a[]=2") - t.Assert(err, nil) + t.AssertNil(err) t.Assert(m, g.Map{ "a": g.Slice{"1", "2"}, }) // map m, err = gstr.Parse("a=1&b=2&c=3") - t.Assert(err, nil) + t.AssertNil(err) t.Assert(m, g.Map{ "a": "1", "b": "2", "c": "3", }) m, err = gstr.Parse("a=1&a=2&c=3") - t.Assert(err, nil) + t.AssertNil(err) t.Assert(m, g.Map{ "a": "2", "c": "3", }) // map m, err = gstr.Parse("m[a]=1&m[b]=2&m[c]=3") - t.Assert(err, nil) + t.AssertNil(err) t.Assert(m, g.Map{ "m": g.Map{ "a": "1", @@ -65,7 +65,7 @@ func Test_Parse(t *testing.T) { }, }) m, err = gstr.Parse("m[a]=1&m[a]=2&m[b]=3") - t.Assert(err, nil) + t.AssertNil(err) t.Assert(m, g.Map{ "m": g.Map{ "a": "2", @@ -74,14 +74,14 @@ func Test_Parse(t *testing.T) { }) // map - slice m, err = gstr.Parse("m[a][]=1&m[a][]=2") - t.Assert(err, nil) + t.AssertNil(err) t.Assert(m, g.Map{ "m": g.Map{ "a": g.Slice{"1", "2"}, }, }) m, err = gstr.Parse("m[a][b][]=1&m[a][b][]=2") - t.Assert(err, nil) + t.AssertNil(err) t.Assert(m, g.Map{ "m": g.Map{ "a": g.Map{ @@ -91,7 +91,7 @@ func Test_Parse(t *testing.T) { }) // map - complicated m, err = gstr.Parse("m[a1][b1][c1][d1]=1&m[a2][b2]=2&m[a3][b3][c3]=3") - t.Assert(err, nil) + t.AssertNil(err) t.Assert(m, g.Map{ "m": g.Map{ "a1": g.Map{ diff --git a/text/gstr/gstr_z_unit_test.go b/text/gstr/gstr_z_unit_test.go index 5744d3ed0..71e020ea9 100644 --- a/text/gstr/gstr_z_unit_test.go +++ b/text/gstr/gstr_z_unit_test.go @@ -80,6 +80,7 @@ func Test_IsNumeric(t *testing.T) { t.Assert(gstr.IsNumeric("1a我"), false) t.Assert(gstr.IsNumeric("0123"), true) t.Assert(gstr.IsNumeric("我是中国人"), false) + t.Assert(gstr.IsNumeric("1.2.3.4"), false) }) } diff --git a/util/gconv/gconv.go b/util/gconv/gconv.go index 47754a3df..b2de0d507 100644 --- a/util/gconv/gconv.go +++ b/util/gconv/gconv.go @@ -10,6 +10,7 @@ package gconv import ( + "context" "fmt" "math" "reflect" @@ -18,8 +19,9 @@ import ( "time" "github.com/gogf/gf/v2/encoding/gbinary" + "github.com/gogf/gf/v2/internal/intlog" "github.com/gogf/gf/v2/internal/json" - "github.com/gogf/gf/v2/internal/utils" + "github.com/gogf/gf/v2/internal/reflection" "github.com/gogf/gf/v2/os/gtime" ) @@ -34,262 +36,11 @@ var ( } // StructTagPriority defines the default priority tags for Map*/Struct* functions. - // Note, the "gconv", "param", "params" tags are used by old version of package. - // It is strongly recommended using short tag "c" or "p" instead in the future. + // Note, the `gconv/param/params` tags are used by old version of package. + // It is strongly recommended using short tag `c/p` instead in the future. StructTagPriority = []string{"gconv", "param", "params", "c", "p", "json"} ) -// Convert converts the variable `fromValue` to the type `toTypeName`, the type `toTypeName` is specified by string. -// The optional parameter `extraParams` is used for additional necessary parameter for this conversion. -// It supports common types conversion as its conversion based on type name string. -func Convert(fromValue interface{}, toTypeName string, extraParams ...interface{}) interface{} { - return doConvert(doConvertInput{ - FromValue: fromValue, - ToTypeName: toTypeName, - ReferValue: nil, - Extra: extraParams, - }) -} - -type doConvertInput struct { - FromValue interface{} // Value that is converted from. - ToTypeName string // Target value type name in string. - ReferValue interface{} // Referred value, a value in type `ToTypeName`. - Extra []interface{} // Extra values for implementing the converting. -} - -// doConvert does commonly use types converting. -func doConvert(in doConvertInput) interface{} { - switch in.ToTypeName { - case "int": - return Int(in.FromValue) - case "*int": - if _, ok := in.FromValue.(*int); ok { - return in.FromValue - } - v := Int(in.FromValue) - return &v - - case "int8": - return Int8(in.FromValue) - case "*int8": - if _, ok := in.FromValue.(*int8); ok { - return in.FromValue - } - v := Int8(in.FromValue) - return &v - - case "int16": - return Int16(in.FromValue) - case "*int16": - if _, ok := in.FromValue.(*int16); ok { - return in.FromValue - } - v := Int16(in.FromValue) - return &v - - case "int32": - return Int32(in.FromValue) - case "*int32": - if _, ok := in.FromValue.(*int32); ok { - return in.FromValue - } - v := Int32(in.FromValue) - return &v - - case "int64": - return Int64(in.FromValue) - case "*int64": - if _, ok := in.FromValue.(*int64); ok { - return in.FromValue - } - v := Int64(in.FromValue) - return &v - - case "uint": - return Uint(in.FromValue) - case "*uint": - if _, ok := in.FromValue.(*uint); ok { - return in.FromValue - } - v := Uint(in.FromValue) - return &v - - case "uint8": - return Uint8(in.FromValue) - case "*uint8": - if _, ok := in.FromValue.(*uint8); ok { - return in.FromValue - } - v := Uint8(in.FromValue) - return &v - - case "uint16": - return Uint16(in.FromValue) - case "*uint16": - if _, ok := in.FromValue.(*uint16); ok { - return in.FromValue - } - v := Uint16(in.FromValue) - return &v - - case "uint32": - return Uint32(in.FromValue) - case "*uint32": - if _, ok := in.FromValue.(*uint32); ok { - return in.FromValue - } - v := Uint32(in.FromValue) - return &v - - case "uint64": - return Uint64(in.FromValue) - case "*uint64": - if _, ok := in.FromValue.(*uint64); ok { - return in.FromValue - } - v := Uint64(in.FromValue) - return &v - - case "float32": - return Float32(in.FromValue) - case "*float32": - if _, ok := in.FromValue.(*float32); ok { - return in.FromValue - } - v := Float32(in.FromValue) - return &v - - case "float64": - return Float64(in.FromValue) - case "*float64": - if _, ok := in.FromValue.(*float64); ok { - return in.FromValue - } - v := Float64(in.FromValue) - return &v - - case "bool": - return Bool(in.FromValue) - case "*bool": - if _, ok := in.FromValue.(*bool); ok { - return in.FromValue - } - v := Bool(in.FromValue) - return &v - - case "string": - return String(in.FromValue) - case "*string": - if _, ok := in.FromValue.(*string); ok { - return in.FromValue - } - v := String(in.FromValue) - return &v - - case "[]byte": - return Bytes(in.FromValue) - case "[]int": - return Ints(in.FromValue) - case "[]int32": - return Int32s(in.FromValue) - case "[]int64": - return Int64s(in.FromValue) - case "[]uint": - return Uints(in.FromValue) - case "[]uint8": - return Bytes(in.FromValue) - case "[]uint32": - return Uint32s(in.FromValue) - case "[]uint64": - return Uint64s(in.FromValue) - case "[]float32": - return Float32s(in.FromValue) - case "[]float64": - return Float64s(in.FromValue) - case "[]string": - return Strings(in.FromValue) - - case "Time", "time.Time": - if len(in.Extra) > 0 { - return Time(in.FromValue, String(in.Extra[0])) - } - return Time(in.FromValue) - case "*time.Time": - var v interface{} - if len(in.Extra) > 0 { - v = Time(in.FromValue, String(in.Extra[0])) - } else { - if _, ok := in.FromValue.(*time.Time); ok { - return in.FromValue - } - v = Time(in.FromValue) - } - return &v - - case "GTime", "gtime.Time": - if len(in.Extra) > 0 { - if v := GTime(in.FromValue, String(in.Extra[0])); v != nil { - return *v - } else { - return *gtime.New() - } - } - if v := GTime(in.FromValue); v != nil { - return *v - } else { - return *gtime.New() - } - case "*gtime.Time": - if len(in.Extra) > 0 { - if v := GTime(in.FromValue, String(in.Extra[0])); v != nil { - return v - } else { - return gtime.New() - } - } - if v := GTime(in.FromValue); v != nil { - return v - } else { - return gtime.New() - } - - case "Duration", "time.Duration": - return Duration(in.FromValue) - case "*time.Duration": - if _, ok := in.FromValue.(*time.Duration); ok { - return in.FromValue - } - v := Duration(in.FromValue) - return &v - - case "map[string]string": - return MapStrStr(in.FromValue) - - case "map[string]interface{}": - return Map(in.FromValue) - - case "[]map[string]interface{}": - return Maps(in.FromValue) - - default: - if in.ReferValue != nil { - var ( - referReflectValue reflect.Value - ) - if v, ok := in.ReferValue.(reflect.Value); ok { - referReflectValue = v - } else { - referReflectValue = reflect.ValueOf(in.ReferValue) - } - in.ToTypeName = referReflectValue.Kind().String() - in.ReferValue = nil - return reflect.ValueOf(doConvert(in)).Convert(referReflectValue.Type()).Interface() - } - return in.FromValue - } -} - // Byte converts `any` to byte. func Byte(any interface{}) byte { if v, ok := any.(byte); ok { @@ -314,8 +65,15 @@ func Bytes(any interface{}) []byte { if f, ok := value.(iBytes); ok { return f.Bytes() } - originValueAndKind := utils.OriginValueAndKind(any) + originValueAndKind := reflection.OriginValueAndKind(any) switch originValueAndKind.OriginKind { + case reflect.Map: + bytes, err := json.Marshal(any) + if err != nil { + intlog.Errorf(context.TODO(), `%+v`, err) + } + return bytes + case reflect.Array, reflect.Slice: var ( ok = true @@ -506,14 +264,18 @@ func checkJsonAndUnmarshalUseNumber(any interface{}, target interface{}) bool { switch r := any.(type) { case []byte: if json.Valid(r) { - _ = json.UnmarshalUseNumber(r, &target) + if err := json.UnmarshalUseNumber(r, &target); err != nil { + return false + } return true } case string: anyAsBytes := []byte(r) if json.Valid(anyAsBytes) { - _ = json.UnmarshalUseNumber(anyAsBytes, &target) + if err := json.UnmarshalUseNumber(anyAsBytes, &target); err != nil { + return false + } return true } } diff --git a/util/gconv/gconv_convert.go b/util/gconv/gconv_convert.go new file mode 100644 index 000000000..baf48b48d --- /dev/null +++ b/util/gconv/gconv_convert.go @@ -0,0 +1,284 @@ +// Copyright GoFrame Author(https://goframe.org). 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 gconv + +import ( + "reflect" + "time" + + "github.com/gogf/gf/v2/os/gtime" +) + +// Convert converts the variable `fromValue` to the type `toTypeName`, the type `toTypeName` is specified by string. +// The optional parameter `extraParams` is used for additional necessary parameter for this conversion. +// It supports common types conversion as its conversion based on type name string. +func Convert(fromValue interface{}, toTypeName string, extraParams ...interface{}) interface{} { + return doConvert(doConvertInput{ + FromValue: fromValue, + ToTypeName: toTypeName, + ReferValue: nil, + Extra: extraParams, + }) +} + +type doConvertInput struct { + FromValue interface{} // Value that is converted from. + ToTypeName string // Target value type name in string. + ReferValue interface{} // Referred value, a value in type `ToTypeName`. + Extra []interface{} // Extra values for implementing the converting. + // Marks that the value is already converted and set to `ReferValue`. Caller can ignore the returned result. + // It is an attribute for internal usage purpose. + alreadySetToReferValue bool +} + +// doConvert does commonly use types converting. +func doConvert(in doConvertInput) (convertedValue interface{}) { + switch in.ToTypeName { + case "int": + return Int(in.FromValue) + case "*int": + if _, ok := in.FromValue.(*int); ok { + return in.FromValue + } + v := Int(in.FromValue) + return &v + + case "int8": + return Int8(in.FromValue) + case "*int8": + if _, ok := in.FromValue.(*int8); ok { + return in.FromValue + } + v := Int8(in.FromValue) + return &v + + case "int16": + return Int16(in.FromValue) + case "*int16": + if _, ok := in.FromValue.(*int16); ok { + return in.FromValue + } + v := Int16(in.FromValue) + return &v + + case "int32": + return Int32(in.FromValue) + case "*int32": + if _, ok := in.FromValue.(*int32); ok { + return in.FromValue + } + v := Int32(in.FromValue) + return &v + + case "int64": + return Int64(in.FromValue) + case "*int64": + if _, ok := in.FromValue.(*int64); ok { + return in.FromValue + } + v := Int64(in.FromValue) + return &v + + case "uint": + return Uint(in.FromValue) + case "*uint": + if _, ok := in.FromValue.(*uint); ok { + return in.FromValue + } + v := Uint(in.FromValue) + return &v + + case "uint8": + return Uint8(in.FromValue) + case "*uint8": + if _, ok := in.FromValue.(*uint8); ok { + return in.FromValue + } + v := Uint8(in.FromValue) + return &v + + case "uint16": + return Uint16(in.FromValue) + case "*uint16": + if _, ok := in.FromValue.(*uint16); ok { + return in.FromValue + } + v := Uint16(in.FromValue) + return &v + + case "uint32": + return Uint32(in.FromValue) + case "*uint32": + if _, ok := in.FromValue.(*uint32); ok { + return in.FromValue + } + v := Uint32(in.FromValue) + return &v + + case "uint64": + return Uint64(in.FromValue) + case "*uint64": + if _, ok := in.FromValue.(*uint64); ok { + return in.FromValue + } + v := Uint64(in.FromValue) + return &v + + case "float32": + return Float32(in.FromValue) + case "*float32": + if _, ok := in.FromValue.(*float32); ok { + return in.FromValue + } + v := Float32(in.FromValue) + return &v + + case "float64": + return Float64(in.FromValue) + case "*float64": + if _, ok := in.FromValue.(*float64); ok { + return in.FromValue + } + v := Float64(in.FromValue) + return &v + + case "bool": + return Bool(in.FromValue) + case "*bool": + if _, ok := in.FromValue.(*bool); ok { + return in.FromValue + } + v := Bool(in.FromValue) + return &v + + case "string": + return String(in.FromValue) + case "*string": + if _, ok := in.FromValue.(*string); ok { + return in.FromValue + } + v := String(in.FromValue) + return &v + + case "[]byte": + return Bytes(in.FromValue) + case "[]int": + return Ints(in.FromValue) + case "[]int32": + return Int32s(in.FromValue) + case "[]int64": + return Int64s(in.FromValue) + case "[]uint": + return Uints(in.FromValue) + case "[]uint8": + return Bytes(in.FromValue) + case "[]uint32": + return Uint32s(in.FromValue) + case "[]uint64": + return Uint64s(in.FromValue) + case "[]float32": + return Float32s(in.FromValue) + case "[]float64": + return Float64s(in.FromValue) + case "[]string": + return Strings(in.FromValue) + + case "Time", "time.Time": + if len(in.Extra) > 0 { + return Time(in.FromValue, String(in.Extra[0])) + } + return Time(in.FromValue) + case "*time.Time": + var v interface{} + if len(in.Extra) > 0 { + v = Time(in.FromValue, String(in.Extra[0])) + } else { + if _, ok := in.FromValue.(*time.Time); ok { + return in.FromValue + } + v = Time(in.FromValue) + } + return &v + + case "GTime", "gtime.Time": + if len(in.Extra) > 0 { + if v := GTime(in.FromValue, String(in.Extra[0])); v != nil { + return *v + } else { + return *gtime.New() + } + } + if v := GTime(in.FromValue); v != nil { + return *v + } else { + return *gtime.New() + } + case "*gtime.Time": + if len(in.Extra) > 0 { + if v := GTime(in.FromValue, String(in.Extra[0])); v != nil { + return v + } else { + return gtime.New() + } + } + if v := GTime(in.FromValue); v != nil { + return v + } else { + return gtime.New() + } + + case "Duration", "time.Duration": + return Duration(in.FromValue) + case "*time.Duration": + if _, ok := in.FromValue.(*time.Duration); ok { + return in.FromValue + } + v := Duration(in.FromValue) + return &v + + case "map[string]string": + return MapStrStr(in.FromValue) + + case "map[string]interface{}": + return Map(in.FromValue) + + case "[]map[string]interface{}": + return Maps(in.FromValue) + + case "json.RawMessage": + return Bytes(in.FromValue) + + default: + if in.ReferValue != nil { + var referReflectValue reflect.Value + if v, ok := in.ReferValue.(reflect.Value); ok { + referReflectValue = v + } else { + referReflectValue = reflect.ValueOf(in.ReferValue) + } + defer func() { + if recover() != nil { + if err := bindVarToReflectValue(referReflectValue, in.FromValue, nil); err == nil { + in.alreadySetToReferValue = true + convertedValue = referReflectValue.Interface() + } + } + }() + in.ToTypeName = referReflectValue.Kind().String() + in.ReferValue = nil + return reflect.ValueOf(doConvert(in)).Convert(referReflectValue.Type()).Interface() + } + return in.FromValue + } +} + +func doConvertWithReflectValueSet(reflectValue reflect.Value, in doConvertInput) { + convertedValue := doConvert(in) + if !in.alreadySetToReferValue { + reflectValue.Set(reflect.ValueOf(convertedValue)) + } +} diff --git a/util/gconv/gconv_int.go b/util/gconv/gconv_int.go index 63bd77529..50626da78 100644 --- a/util/gconv/gconv_int.go +++ b/util/gconv/gconv_int.go @@ -97,8 +97,10 @@ func Int64(any interface{}) int64 { if f, ok := value.(iInt64); ok { return f.Int64() } - s := String(value) - isMinus := false + var ( + s = String(value) + isMinus = false + ) if len(s) > 0 { if s[0] == '-' { isMinus = true @@ -116,15 +118,6 @@ func Int64(any interface{}) int64 { return v } } - // Octal - if len(s) > 1 && s[0] == '0' { - if v, e := strconv.ParseInt(s[1:], 8, 64); e == nil { - if isMinus { - return -v - } - return v - } - } // Decimal if v, e := strconv.ParseInt(s, 10, 64); e == nil { if isMinus { diff --git a/util/gconv/gconv_map.go b/util/gconv/gconv_map.go index c42f8e007..3c2093283 100644 --- a/util/gconv/gconv_map.go +++ b/util/gconv/gconv_map.go @@ -217,23 +217,25 @@ func doMapConvertForMapOrStructValue(isRoot bool, value interface{}, recursive b return dataMap case reflect.Struct: + var dataMap = make(map[string]interface{}) // Map converting interface check. if v, ok := value.(iMapStrAny); ok { - m := v.MapStrAny() - if recursive { - for k, v := range m { - m[k] = doMapConvertForMapOrStructValue(false, v, recursive, tags...) + // Value copy, in case of concurrent safety. + for mapK, mapV := range v.MapStrAny() { + if recursive { + dataMap[mapK] = doMapConvertForMapOrStructValue(false, mapV, recursive, tags...) + } else { + dataMap[mapK] = mapV } } - return m + return dataMap } // Using reflect for converting. var ( rtField reflect.StructField rvField reflect.Value - dataMap = make(map[string]interface{}) // result map. - reflectType = reflectValue.Type() // attribute value type. - mapKey = "" // mapKey may be the tag name or the struct attribute name. + reflectType = reflectValue.Type() // attribute value type. + mapKey = "" // mapKey may be the tag name or the struct attribute name. ) for i := 0; i < reflectValue.NumField(); i++ { rtField = reflectType.Field(i) @@ -319,11 +321,26 @@ func doMapConvertForMapOrStructValue(isRoot bool, value interface{}, recursive b break } array := make([]interface{}, length) - for i := 0; i < length; i++ { - array[i] = doMapConvertForMapOrStructValue(false, rvAttrField.Index(i), recursive, tags...) + for arrayIndex := 0; arrayIndex < length; arrayIndex++ { + array[arrayIndex] = doMapConvertForMapOrStructValue( + false, rvAttrField.Index(arrayIndex), recursive, tags..., + ) } dataMap[mapKey] = array - + case reflect.Map: + var ( + mapKeys = rvAttrField.MapKeys() + nestedMap = make(map[string]interface{}) + ) + for _, k := range mapKeys { + nestedMap[String(k.Interface())] = doMapConvertForMapOrStructValue( + false, + rvAttrField.MapIndex(k).Interface(), + recursive, + tags..., + ) + } + dataMap[mapKey] = nestedMap default: if rvField.IsValid() { dataMap[mapKey] = reflectValue.Field(i).Interface() diff --git a/util/gconv/gconv_slice_any.go b/util/gconv/gconv_slice_any.go index d4f73f610..415f47634 100644 --- a/util/gconv/gconv_slice_any.go +++ b/util/gconv/gconv_slice_any.go @@ -9,7 +9,8 @@ package gconv import ( "reflect" - "github.com/gogf/gf/v2/internal/utils" + "github.com/gogf/gf/v2/internal/json" + "github.com/gogf/gf/v2/internal/reflection" ) // SliceAny is alias of Interfaces. @@ -22,13 +23,10 @@ func Interfaces(any interface{}) []interface{} { if any == nil { return nil } - if r, ok := any.([]interface{}); ok { - return r - } - var ( - array []interface{} = nil - ) + var array []interface{} switch value := any.(type) { + case []interface{}: + array = value case []string: array = make([]interface{}, len(value)) for k, v := range value { @@ -65,9 +63,13 @@ func Interfaces(any interface{}) []interface{} { array[k] = v } case []uint8: - array = make([]interface{}, len(value)) - for k, v := range value { - array[k] = v + if json.Valid(value) { + _ = json.UnmarshalUseNumber(value, &array) + } else { + array = make([]interface{}, len(value)) + for k, v := range value { + array[k] = v + } } case []uint16: array = make([]interface{}, len(value)) @@ -110,7 +112,7 @@ func Interfaces(any interface{}) []interface{} { return array } // Not a common type, it then uses reflection for conversion. - originValueAndKind := utils.OriginValueAndKind(any) + originValueAndKind := reflection.OriginValueAndKind(any) switch originValueAndKind.OriginKind { case reflect.Slice, reflect.Array: var ( diff --git a/util/gconv/gconv_slice_float.go b/util/gconv/gconv_slice_float.go index 56c774e33..3d7b49949 100644 --- a/util/gconv/gconv_slice_float.go +++ b/util/gconv/gconv_slice_float.go @@ -9,7 +9,8 @@ package gconv import ( "reflect" - "github.com/gogf/gf/v2/internal/utils" + "github.com/gogf/gf/v2/internal/json" + "github.com/gogf/gf/v2/internal/reflection" ) // SliceFloat is alias of Floats. @@ -81,9 +82,13 @@ func Float32s(any interface{}) []float32 { array = append(array, Float32(v)) } case []uint8: - array = make([]float32, len(value)) - for k, v := range value { - array[k] = Float32(v) + if json.Valid(value) { + _ = json.UnmarshalUseNumber(value, &array) + } else { + array = make([]float32, len(value)) + for k, v := range value { + array[k] = Float32(v) + } } case []uint16: array = make([]float32, len(value)) @@ -132,7 +137,7 @@ func Float32s(any interface{}) []float32 { return array } // Not a common type, it then uses reflection for conversion. - originValueAndKind := utils.OriginValueAndKind(any) + originValueAndKind := reflection.OriginValueAndKind(any) switch originValueAndKind.OriginKind { case reflect.Slice, reflect.Array: var ( @@ -201,9 +206,13 @@ func Float64s(any interface{}) []float64 { array = append(array, Float64(v)) } case []uint8: - array = make([]float64, len(value)) - for k, v := range value { - array[k] = Float64(v) + if json.Valid(value) { + _ = json.UnmarshalUseNumber(value, &array) + } else { + array = make([]float64, len(value)) + for k, v := range value { + array[k] = Float64(v) + } } case []uint16: array = make([]float64, len(value)) @@ -252,7 +261,7 @@ func Float64s(any interface{}) []float64 { return array } // Not a common type, it then uses reflection for conversion. - originValueAndKind := utils.OriginValueAndKind(any) + originValueAndKind := reflection.OriginValueAndKind(any) switch originValueAndKind.OriginKind { case reflect.Slice, reflect.Array: var ( diff --git a/util/gconv/gconv_slice_int.go b/util/gconv/gconv_slice_int.go index c34701bb9..f28e7fd18 100644 --- a/util/gconv/gconv_slice_int.go +++ b/util/gconv/gconv_slice_int.go @@ -9,7 +9,8 @@ package gconv import ( "reflect" - "github.com/gogf/gf/v2/internal/utils" + "github.com/gogf/gf/v2/internal/json" + "github.com/gogf/gf/v2/internal/reflection" ) // SliceInt is alias of Ints. @@ -69,9 +70,13 @@ func Ints(any interface{}) []int { array[k] = int(v) } case []uint8: - array = make([]int, len(value)) - for k, v := range value { - array[k] = int(v) + if json.Valid(value) { + _ = json.UnmarshalUseNumber(value, &array) + } else { + array = make([]int, len(value)) + for k, v := range value { + array[k] = int(v) + } } case []uint16: array = make([]int, len(value)) @@ -132,7 +137,7 @@ func Ints(any interface{}) []int { return array } // Not a common type, it then uses reflection for conversion. - originValueAndKind := utils.OriginValueAndKind(any) + originValueAndKind := reflection.OriginValueAndKind(any) switch originValueAndKind.OriginKind { case reflect.Slice, reflect.Array: var ( @@ -194,9 +199,13 @@ func Int32s(any interface{}) []int32 { array[k] = int32(v) } case []uint8: - array = make([]int32, len(value)) - for k, v := range value { - array[k] = int32(v) + if json.Valid(value) { + _ = json.UnmarshalUseNumber(value, &array) + } else { + array = make([]int32, len(value)) + for k, v := range value { + array[k] = int32(v) + } } case []uint16: array = make([]int32, len(value)) @@ -257,7 +266,7 @@ func Int32s(any interface{}) []int32 { return array } // Not a common type, it then uses reflection for conversion. - originValueAndKind := utils.OriginValueAndKind(any) + originValueAndKind := reflection.OriginValueAndKind(any) switch originValueAndKind.OriginKind { case reflect.Slice, reflect.Array: var ( @@ -319,9 +328,13 @@ func Int64s(any interface{}) []int64 { array[k] = int64(v) } case []uint8: - array = make([]int64, len(value)) - for k, v := range value { - array[k] = int64(v) + if json.Valid(value) { + _ = json.UnmarshalUseNumber(value, &array) + } else { + array = make([]int64, len(value)) + for k, v := range value { + array[k] = int64(v) + } } case []uint16: array = make([]int64, len(value)) @@ -382,7 +395,7 @@ func Int64s(any interface{}) []int64 { return array } // Not a common type, it then uses reflection for conversion. - originValueAndKind := utils.OriginValueAndKind(any) + originValueAndKind := reflection.OriginValueAndKind(any) switch originValueAndKind.OriginKind { case reflect.Slice, reflect.Array: var ( diff --git a/util/gconv/gconv_slice_str.go b/util/gconv/gconv_slice_str.go index 9118bd522..c085d271f 100644 --- a/util/gconv/gconv_slice_str.go +++ b/util/gconv/gconv_slice_str.go @@ -9,7 +9,8 @@ package gconv import ( "reflect" - "github.com/gogf/gf/v2/internal/utils" + "github.com/gogf/gf/v2/internal/json" + "github.com/gogf/gf/v2/internal/reflection" ) // SliceStr is alias of Strings. @@ -57,9 +58,13 @@ func Strings(any interface{}) []string { array[k] = String(v) } case []uint8: - array = make([]string, len(value)) - for k, v := range value { - array[k] = String(v) + if json.Valid(value) { + _ = json.UnmarshalUseNumber(value, &array) + } else { + array = make([]string, len(value)) + for k, v := range value { + array[k] = String(v) + } } case []uint16: array = make([]string, len(value)) @@ -118,7 +123,7 @@ func Strings(any interface{}) []string { return array } // Not a common type, it then uses reflection for conversion. - originValueAndKind := utils.OriginValueAndKind(any) + originValueAndKind := reflection.OriginValueAndKind(any) switch originValueAndKind.OriginKind { case reflect.Slice, reflect.Array: var ( diff --git a/util/gconv/gconv_slice_uint.go b/util/gconv/gconv_slice_uint.go index 01d7a1d97..a1ffa7617 100644 --- a/util/gconv/gconv_slice_uint.go +++ b/util/gconv/gconv_slice_uint.go @@ -10,6 +10,8 @@ import ( "reflect" "strings" + "github.com/gogf/gf/v2/internal/json" + "github.com/gogf/gf/v2/internal/reflection" "github.com/gogf/gf/v2/internal/utils" ) @@ -75,9 +77,13 @@ func Uints(any interface{}) []uint { case []uint: array = value case []uint8: - array = make([]uint, len(value)) - for k, v := range value { - array[k] = uint(v) + if json.Valid(value) { + _ = json.UnmarshalUseNumber(value, &array) + } else { + array = make([]uint, len(value)) + for k, v := range value { + array[k] = uint(v) + } } case []uint16: array = make([]uint, len(value)) @@ -141,7 +147,7 @@ func Uints(any interface{}) []uint { return array } // Not a common type, it then uses reflection for conversion. - originValueAndKind := utils.OriginValueAndKind(any) + originValueAndKind := reflection.OriginValueAndKind(any) switch originValueAndKind.OriginKind { case reflect.Slice, reflect.Array: var ( @@ -209,9 +215,13 @@ func Uint32s(any interface{}) []uint32 { array[k] = uint32(v) } case []uint8: - array = make([]uint32, len(value)) - for k, v := range value { - array[k] = uint32(v) + if json.Valid(value) { + _ = json.UnmarshalUseNumber(value, &array) + } else { + array = make([]uint32, len(value)) + for k, v := range value { + array[k] = uint32(v) + } } case []uint16: array = make([]uint32, len(value)) @@ -271,7 +281,7 @@ func Uint32s(any interface{}) []uint32 { return array } // Not a common type, it then uses reflection for conversion. - originValueAndKind := utils.OriginValueAndKind(any) + originValueAndKind := reflection.OriginValueAndKind(any) switch originValueAndKind.OriginKind { case reflect.Slice, reflect.Array: var ( @@ -340,9 +350,13 @@ func Uint64s(any interface{}) []uint64 { array[k] = uint64(v) } case []uint8: - array = make([]uint64, len(value)) - for k, v := range value { - array[k] = uint64(v) + if json.Valid(value) { + _ = json.UnmarshalUseNumber(value, &array) + } else { + array = make([]uint64, len(value)) + for k, v := range value { + array[k] = uint64(v) + } } case []uint16: array = make([]uint64, len(value)) @@ -401,7 +415,7 @@ func Uint64s(any interface{}) []uint64 { return array } // Not a common type, it then uses reflection for conversion. - originValueAndKind := utils.OriginValueAndKind(any) + originValueAndKind := reflection.OriginValueAndKind(any) switch originValueAndKind.OriginKind { case reflect.Slice, reflect.Array: var ( diff --git a/util/gconv/gconv_struct.go b/util/gconv/gconv_struct.go index 03b3a5839..b9eb2f95c 100644 --- a/util/gconv/gconv_struct.go +++ b/util/gconv/gconv_struct.go @@ -192,11 +192,11 @@ func doStruct(params interface{}, pointer interface{}, mapping map[string]string // The key of the attrMap is the attribute name of the struct, // and the value is its replaced name for later comparison to improve performance. var ( - tempName string - elemFieldType reflect.StructField - elemFieldValue reflect.Value - elemType = pointerElemReflectValue.Type() - attrMap = make(map[string]string) + tempName string + elemFieldType reflect.StructField + elemFieldValue reflect.Value + elemType = pointerElemReflectValue.Type() + attrToCheckNameMap = make(map[string]string) ) for i := 0; i < pointerElemReflectValue.NumField(); i++ { elemFieldType = elemType.Field(i) @@ -219,66 +219,77 @@ func doStruct(params interface{}, pointer interface{}, mapping map[string]string } } else { tempName = elemFieldType.Name - attrMap[tempName] = utils.RemoveSymbols(tempName) + attrToCheckNameMap[tempName] = utils.RemoveSymbols(tempName) } } - if len(attrMap) == 0 { + if len(attrToCheckNameMap) == 0 { return nil } // The key of the tagMap is the attribute name of the struct, // and the value is its replaced tag name for later comparison to improve performance. var ( - tagMap = make(map[string]string) - priorityTagArray []string + attrToTagCheckNameMap = make(map[string]string) + priorityTagArray []string ) if priorityTag != "" { priorityTagArray = append(utils.SplitAndTrim(priorityTag, ","), StructTagPriority...) } else { priorityTagArray = StructTagPriority } - tagToNameMap, err := gstructs.TagMapName(pointerElemReflectValue, priorityTagArray) + tagToAttrNameMap, err := gstructs.TagMapName(pointerElemReflectValue, priorityTagArray) if err != nil { return err } - for tagName, attributeName := range tagToNameMap { + for tagName, attributeName := range tagToAttrNameMap { // If there's something else in the tag string, // it uses the first part which is split using char ','. // Eg: // orm:"id, priority" // orm:"name, with:uid=id" - tagMap[attributeName] = utils.RemoveSymbols(strings.Split(tagName, ",")[0]) + attrToTagCheckNameMap[attributeName] = utils.RemoveSymbols(strings.Split(tagName, ",")[0]) + // If tag and attribute values both exist in `paramsMap`, + // it then uses the tag value overwriting the attribute value in `paramsMap`. + if paramsMap[tagName] != nil && paramsMap[attributeName] != nil { + paramsMap[attributeName] = paramsMap[tagName] + } } var ( attrName string checkName string ) - for mapK, mapV := range paramsMap { + for paramName, paramValue := range paramsMap { attrName = "" // It firstly checks the passed mapping rules. if len(mapping) > 0 { - if passedAttrKey, ok := mapping[mapK]; ok { + if passedAttrKey, ok := mapping[paramName]; ok { attrName = passedAttrKey } } // It secondly checks the predefined tags and matching rules. if attrName == "" { - checkName = utils.RemoveSymbols(mapK) - // Loop to find the matched attribute name with or without - // string cases and chars like '-'/'_'/'.'/' '. + // It firstly considers `paramName` as accurate tag name, + // and retrieve attribute name from `tagToAttrNameMap` . + attrName = tagToAttrNameMap[paramName] + if attrName == "" { + checkName = utils.RemoveSymbols(paramName) + // Loop to find the matched attribute name with or without + // string cases and chars like '-'/'_'/'.'/' '. - // Matching the parameters to struct tag names. - // The `tagV` is the attribute name of the struct. - for attrKey, cmpKey := range tagMap { - if strings.EqualFold(checkName, cmpKey) { - attrName = attrKey - break + // Matching the parameters to struct tag names. + // The `attrKey` is the attribute name of the struct. + for attrKey, cmpKey := range attrToTagCheckNameMap { + if strings.EqualFold(checkName, cmpKey) { + attrName = attrKey + break + } } } + // Matching the parameters to struct attributes. if attrName == "" { - for attrKey, cmpKey := range attrMap { + for attrKey, cmpKey := range attrToCheckNameMap { // Eg: // UserName eq user_name // User-Name eq username @@ -297,12 +308,12 @@ func doStruct(params interface{}, pointer interface{}, mapping map[string]string continue } // If the attribute name is already checked converting, then skip it. - if _, ok := doneMap[attrName]; ok { + if _, ok = doneMap[attrName]; ok { continue } // Mark it done. doneMap[attrName] = struct{}{} - if err := bindVarToStructAttr(pointerElemReflectValue, attrName, mapV, mapping); err != nil { + if err = bindVarToStructAttr(pointerElemReflectValue, attrName, paramValue, mapping); err != nil { return err } } @@ -310,8 +321,8 @@ func doStruct(params interface{}, pointer interface{}, mapping map[string]string } // bindVarToStructAttr sets value to struct object attribute by name. -func bindVarToStructAttr(elem reflect.Value, name string, value interface{}, mapping map[string]string) (err error) { - structFieldValue := elem.FieldByName(name) +func bindVarToStructAttr(structReflectValue reflect.Value, attrName string, value interface{}, mapping map[string]string) (err error) { + structFieldValue := structReflectValue.FieldByName(attrName) if !structFieldValue.IsValid() { return nil } @@ -322,7 +333,7 @@ func bindVarToStructAttr(elem reflect.Value, name string, value interface{}, map defer func() { if exception := recover(); exception != nil { if err = bindVarToReflectValue(structFieldValue, value, mapping); err != nil { - err = gerror.Wrapf(err, `error binding value to attribute "%s"`, name) + err = gerror.Wrapf(err, `error binding value to attribute "%s"`, attrName) } } }() @@ -330,13 +341,17 @@ func bindVarToStructAttr(elem reflect.Value, name string, value interface{}, map if empty.IsNil(value) { structFieldValue.Set(reflect.Zero(structFieldValue.Type())) } else { - structFieldValue.Set(reflect.ValueOf(doConvert( - doConvertInput{ - FromValue: value, - ToTypeName: structFieldValue.Type().String(), - ReferValue: structFieldValue, - }, - ))) + // Common interface check. + var ok bool + if err, ok = bindVarToReflectValueWithInterfaceCheck(structFieldValue, value); ok { + return err + } + // Default converting. + doConvertWithReflectValueSet(structFieldValue, doConvertInput{ + FromValue: value, + ToTypeName: structFieldValue.Type().String(), + ReferValue: structFieldValue, + }) } return nil } @@ -412,13 +427,8 @@ func bindVarToReflectValue(structFieldValue reflect.Value, value interface{}, ma return nil } - // Common interface check. - if err, ok := bindVarToReflectValueWithInterfaceCheck(structFieldValue, value); ok { - return err - } - kind := structFieldValue.Kind() - // Converting using interface, for some kinds. + // Converting using `Set` interface implements, for some types. switch kind { case reflect.Slice, reflect.Array, reflect.Ptr, reflect.Interface: if !structFieldValue.IsNil() { @@ -429,7 +439,7 @@ func bindVarToReflectValue(structFieldValue reflect.Value, value interface{}, ma } } - // Converting by kind. + // Converting using reflection by kind. switch kind { case reflect.Map: return doMapToMap(value, structFieldValue, mapping) @@ -444,55 +454,88 @@ func bindVarToReflectValue(structFieldValue reflect.Value, value interface{}, ma // Note that the slice element might be type of struct, // so it uses Struct function doing the converting internally. case reflect.Slice, reflect.Array: - a := reflect.Value{} - v := reflect.ValueOf(value) - if v.Kind() == reflect.Slice || v.Kind() == reflect.Array { - a = reflect.MakeSlice(structFieldValue.Type(), v.Len(), v.Len()) - if v.Len() > 0 { - t := a.Index(0).Type() - for i := 0; i < v.Len(); i++ { - if t.Kind() == reflect.Ptr { - e := reflect.New(t.Elem()).Elem() - if err = doStruct(v.Index(i).Interface(), e, nil, ""); err != nil { - // Note there's reflect conversion mechanism here. - e.Set(reflect.ValueOf(v.Index(i).Interface()).Convert(t)) - } - a.Index(i).Set(e.Addr()) - } else { - e := reflect.New(t).Elem() - if err = doStruct(v.Index(i).Interface(), e, nil, ""); err != nil { - // Note there's reflect conversion mechanism here. - e.Set(reflect.ValueOf(v.Index(i).Interface()).Convert(t)) - } - a.Index(i).Set(e) + var ( + reflectArray reflect.Value + reflectValue = reflect.ValueOf(value) + ) + if reflectValue.Kind() == reflect.Slice || reflectValue.Kind() == reflect.Array { + reflectArray = reflect.MakeSlice(structFieldValue.Type(), reflectValue.Len(), reflectValue.Len()) + if reflectValue.Len() > 0 { + var ( + elemType = reflectArray.Index(0).Type() + elemTypeName string + converted bool + ) + for i := 0; i < reflectValue.Len(); i++ { + converted = false + elemTypeName = elemType.Name() + if elemTypeName == "" { + elemTypeName = elemType.String() } + var elem reflect.Value + if elemType.Kind() == reflect.Ptr { + elem = reflect.New(elemType.Elem()).Elem() + } else { + elem = reflect.New(elemType).Elem() + } + if elem.Kind() == reflect.Struct { + if err = doStruct(reflectValue.Index(i).Interface(), elem, nil, ""); err == nil { + converted = true + } + } + if !converted { + doConvertWithReflectValueSet(elem, doConvertInput{ + FromValue: reflectValue.Index(i).Interface(), + ToTypeName: elemTypeName, + ReferValue: elem, + }) + } + if elemType.Kind() == reflect.Ptr { + // Before it sets the `elem` to array, do pointer converting if necessary. + elem = elem.Addr() + } + reflectArray.Index(i).Set(elem) } } } else { - a = reflect.MakeSlice(structFieldValue.Type(), 1, 1) - t := a.Index(0).Type() - if t.Kind() == reflect.Ptr { - // Pointer element. - e := reflect.New(t.Elem()).Elem() - if err = doStruct(value, e, nil, ""); err != nil { - // Note there's reflect conversion mechanism here. - e.Set(reflect.ValueOf(value).Convert(t)) - } - a.Index(0).Set(e.Addr()) - } else { - // Just consider it as struct element. (Although it might be other types but not basic types, eg: map) - e := reflect.New(t).Elem() - if err = doStruct(value, e, nil, ""); err != nil { - return err - } - a.Index(0).Set(e) + reflectArray = reflect.MakeSlice(structFieldValue.Type(), 1, 1) + var ( + elem reflect.Value + elemType = reflectArray.Index(0).Type() + elemTypeName = elemType.Name() + converted bool + ) + if elemTypeName == "" { + elemTypeName = elemType.String() } + if elemType.Kind() == reflect.Ptr { + elem = reflect.New(elemType.Elem()).Elem() + } else { + elem = reflect.New(elemType).Elem() + } + if elem.Kind() == reflect.Struct { + if err = doStruct(value, elem, nil, ""); err == nil { + converted = true + } + } + if !converted { + doConvertWithReflectValueSet(elem, doConvertInput{ + FromValue: value, + ToTypeName: elemTypeName, + ReferValue: elem, + }) + } + if elemType.Kind() == reflect.Ptr { + // Before it sets the `elem` to array, do pointer converting if necessary. + elem = elem.Addr() + } + reflectArray.Index(0).Set(elem) } - structFieldValue.Set(a) + structFieldValue.Set(reflectArray) case reflect.Ptr: item := reflect.New(structFieldValue.Type().Elem()) - if err, ok := bindVarToReflectValueWithInterfaceCheck(item, value); ok { + if err, ok = bindVarToReflectValueWithInterfaceCheck(item, value); ok { structFieldValue.Set(item) return err } diff --git a/util/gconv/gconv_structs.go b/util/gconv/gconv_structs.go index bb73319fe..be1322a72 100644 --- a/util/gconv/gconv_structs.go +++ b/util/gconv/gconv_structs.go @@ -48,7 +48,7 @@ func doStructs(params interface{}, pointer interface{}, mapping map[string]strin } defer func() { - // Catch the panic, especially the reflect operation panics. + // Catch the panic, especially the reflection operation panics. if exception := recover(); exception != nil { if v, ok := exception.(error); ok && gerror.HasStack(v) { err = v @@ -102,7 +102,7 @@ func doStructs(params interface{}, pointer interface{}, mapping map[string]strin case reflect.Slice, reflect.Array: paramsList = make([]interface{}, paramsRv.Len()) for i := 0; i < paramsRv.Len(); i++ { - paramsList[i] = paramsRv.Index(i) + paramsList[i] = paramsRv.Index(i).Interface() } default: var paramsMaps = Maps(params) diff --git a/util/gconv/gconv_uint.go b/util/gconv/gconv_uint.go index 4f8d3b63a..fd0c25854 100644 --- a/util/gconv/gconv_uint.go +++ b/util/gconv/gconv_uint.go @@ -104,12 +104,6 @@ func Uint64(any interface{}) uint64 { return v } } - // Octal - if len(s) > 1 && s[0] == '0' { - if v, e := strconv.ParseUint(s[1:], 8, 64); e == nil { - return v - } - } // Decimal if v, e := strconv.ParseUint(s, 10, 64); e == nil { return v diff --git a/util/gconv/gconv_z_unit_all_test.go b/util/gconv/gconv_z_unit_all_test.go index d42351889..87b0f430f 100644 --- a/util/gconv/gconv_z_unit_all_test.go +++ b/util/gconv/gconv_z_unit_all_test.go @@ -36,6 +36,103 @@ func (s1 S1) Error() string { return "22222" } +// https://github.com/gogf/gf/issues/1227 +func Test_Issue1227(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + type StructFromIssue1227 struct { + Name string `json:"n1"` + } + tests := []struct { + name string + origin interface{} + want string + }{ + { + name: "Case1", + origin: `{"n1":"n1"}`, + want: "n1", + }, + { + name: "Case2", + origin: `{"name":"name"}`, + want: "", + }, + { + name: "Case3", + origin: `{"NaMe":"NaMe"}`, + want: "", + }, + { + name: "Case4", + origin: g.Map{"n1": "n1"}, + want: "n1", + }, + { + name: "Case5", + origin: g.Map{"NaMe": "n1"}, + want: "n1", + }, + } + for _, tt := range tests { + p := StructFromIssue1227{} + if err := gconv.Struct(tt.origin, &p); err != nil { + t.Error(err) + } + t.Assert(p.Name, tt.want) + } + }) + + // Chinese key. + gtest.C(t, func(t *gtest.T) { + type StructFromIssue1227 struct { + Name string `json:"中文Key"` + } + tests := []struct { + name string + origin interface{} + want string + }{ + { + name: "Case1", + origin: `{"中文Key":"n1"}`, + want: "n1", + }, + { + name: "Case2", + origin: `{"Key":"name"}`, + want: "", + }, + { + name: "Case3", + origin: `{"NaMe":"NaMe"}`, + want: "", + }, + { + name: "Case4", + origin: g.Map{"中文Key": "n1"}, + want: "n1", + }, + { + name: "Case5", + origin: g.Map{"中文KEY": "n1"}, + want: "n1", + }, + { + name: "Case5", + origin: g.Map{"KEY": "n1"}, + want: "", + }, + } + for _, tt := range tests { + p := StructFromIssue1227{} + if err := gconv.Struct(tt.origin, &p); err != nil { + t.Error(err) + } + t.Assert(p.Name, tt.want) + } + }) +} + func Test_Bool_All(t *testing.T) { gtest.C(t, func(t *gtest.T) { var any interface{} = nil @@ -211,7 +308,7 @@ func Test_Int64_All(t *testing.T) { gtest.C(t, func(t *gtest.T) { var any interface{} = nil t.AssertEQ(gconv.Int64("0x00e"), int64(14)) - t.Assert(gconv.Int64("022"), int64(18)) + t.Assert(gconv.Int64("022"), int64(22)) t.Assert(gconv.Int64(any), int64(0)) t.Assert(gconv.Int64(true), 1) @@ -405,7 +502,7 @@ func Test_Uint64_All(t *testing.T) { gtest.C(t, func(t *gtest.T) { var any interface{} = nil t.AssertEQ(gconv.Uint64("0x00e"), uint64(14)) - t.Assert(gconv.Uint64("022"), uint64(18)) + t.Assert(gconv.Uint64("022"), uint64(22)) t.AssertEQ(gconv.Uint64(any), uint64(0)) t.AssertEQ(gconv.Uint64(true), uint64(1)) @@ -1269,7 +1366,7 @@ func Test_Struct_PrivateAttribute_All(t *testing.T) { gtest.C(t, func(t *gtest.T) { user := new(User) err := gconv.Struct(g.Map{"id": 1, "name": "john"}, user) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(user.Id, 1) t.Assert(user.name, "") }) diff --git a/util/gconv/gconv_z_unit_custom_type_test.go b/util/gconv/gconv_z_unit_custom_type_test.go index 1bf79cd87..d6b168b63 100644 --- a/util/gconv/gconv_z_unit_custom_type_test.go +++ b/util/gconv/gconv_z_unit_custom_type_test.go @@ -36,6 +36,6 @@ func Test_Struct_CustomTimeDuration_Attribute(t *testing.T) { "name": "john", "timeout": "1s", }, &a) - t.Assert(err, nil) + t.AssertNil(err) }) } diff --git a/util/gconv/gconv_z_unit_map_test.go b/util/gconv/gconv_z_unit_map_test.go index 000f439bd..634d0ebd1 100644 --- a/util/gconv/gconv_z_unit_map_test.go +++ b/util/gconv/gconv_z_unit_map_test.go @@ -7,7 +7,9 @@ package gconv_test import ( + "encoding/json" "github.com/gogf/gf/v2/util/gutil" + "gopkg.in/yaml.v3" "testing" "github.com/gogf/gf/v2/frame/g" @@ -407,3 +409,70 @@ func Test_MapDeepWithAttributeTag(t *testing.T) { t.Assert(m["base"].(map[string]interface{})["create_time"], user.CreateTime) }) } + +func Test_MapDeepWithNestedMapAnyAny(t *testing.T) { + type User struct { + ExtraAttributes g.Map `c:"extra_attributes"` + } + + gtest.C(t, func(t *gtest.T) { + user := &User{ + ExtraAttributes: g.Map{ + "simple_attribute": 123, + "map_string_attribute": g.Map{ + "inner_value": 456, + }, + "map_interface_attribute": g.MapAnyAny{ + "inner_value": 456, + 123: "integer_key_should_be_converted_to_string", + }, + }, + } + m := gconv.MapDeep(user) + t.Assert(m, g.Map{ + "extra_attributes": g.Map{ + "simple_attribute": 123, + "map_string_attribute": g.Map{ + "inner_value": user.ExtraAttributes["map_string_attribute"].(g.Map)["inner_value"], + }, + "map_interface_attribute": g.Map{ + "inner_value": user.ExtraAttributes["map_interface_attribute"].(g.MapAnyAny)["inner_value"], + "123": "integer_key_should_be_converted_to_string", + }, + }, + }) + }) + + type Outer struct { + OuterStruct map[string]interface{} `c:"outer_struct" yaml:"outer_struct"` + Field3 map[string]interface{} `c:"field3" yaml:"field3"` + } + + gtest.C(t, func(t *gtest.T) { + problemYaml := []byte(` +outer_struct: + field1: &anchor1 + inner1: 123 + inner2: 345 + field2: + inner3: 456 + inner4: 789 + <<: *anchor1 +field3: + 123: integer_key +`) + parsed := &Outer{} + + err := yaml.Unmarshal(problemYaml, parsed) + t.AssertNil(err) + + _, err = json.Marshal(parsed) + t.Assert(err.Error(), "json: unsupported type: map[interface {}]interface {}") + + converted := gconv.MapDeep(parsed) + jsonData, err := json.Marshal(converted) + t.AssertNil(err) + + t.Assert(string(jsonData), `{"field3":{"123":"integer_key"},"outer_struct":{"field1":{"inner1":123,"inner2":345},"field2":{"inner1":123,"inner2":345,"inner3":456,"inner4":789}}}`) + }) +} diff --git a/util/gconv/gconv_z_unit_maptomap_test.go b/util/gconv/gconv_z_unit_maptomap_test.go index ea0aa9db2..78500ec99 100644 --- a/util/gconv/gconv_z_unit_maptomap_test.go +++ b/util/gconv/gconv_z_unit_maptomap_test.go @@ -91,7 +91,7 @@ func Test_MapToMap2(t *testing.T) { gtest.C(t, func(t *gtest.T) { m := (map[string]User)(nil) err := gconv.MapToMap(params, &m) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(len(m), 1) t.Assert(m["key"].Id, 1) t.Assert(m["key"].Name, "john") @@ -99,7 +99,7 @@ func Test_MapToMap2(t *testing.T) { gtest.C(t, func(t *gtest.T) { m := make(map[string]*User) err := gconv.MapToMap(params, &m) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(len(m), 1) t.Assert(m["key"].Id, 1) t.Assert(m["key"].Name, "john") @@ -107,7 +107,7 @@ func Test_MapToMap2(t *testing.T) { gtest.C(t, func(t *gtest.T) { m := (map[string]*User)(nil) err := gconv.MapToMap(params, &m) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(len(m), 1) t.Assert(m["key"].Id, 1) t.Assert(m["key"].Name, "john") @@ -136,7 +136,7 @@ func Test_MapToMapDeep(t *testing.T) { gtest.C(t, func(t *gtest.T) { m := (map[string]*User)(nil) err := gconv.MapToMap(params, &m) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(len(m), 1) t.Assert(m["key"].Id, 1) t.Assert(m["key"].Name, "john") diff --git a/util/gconv/gconv_z_unit_scan_test.go b/util/gconv/gconv_z_unit_scan_test.go index e34f91d99..f94bf3e1a 100644 --- a/util/gconv/gconv_z_unit_scan_test.go +++ b/util/gconv/gconv_z_unit_scan_test.go @@ -8,11 +8,13 @@ package gconv_test import ( "fmt" + "math/big" + "testing" + "github.com/gogf/gf/v2/container/gvar" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/test/gtest" "github.com/gogf/gf/v2/util/gconv" - "testing" ) func Test_Scan_StructStructs(t *testing.T) { @@ -33,7 +35,7 @@ func Test_Scan_StructStructs(t *testing.T) { } ) err := gconv.Scan(params, user) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(user, &User{ Uid: 1, Name: "john", @@ -91,7 +93,7 @@ func Test_Scan_StructStr(t *testing.T) { params = `{"uid":1,"name":"john", "pass1":"123","pass2":"456"}` ) err := gconv.Scan(params, user) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(user, &User{ Uid: 1, Name: "john", @@ -108,7 +110,7 @@ func Test_Scan_StructStr(t *testing.T) { ]` ) err := gconv.Scan(params, &users) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(users, g.Slice{ &User{ Uid: 1, @@ -601,3 +603,30 @@ func Test_ScanList_Embedded(t *testing.T) { t.Assert(len(entities[2].UserScores), 0) }) } + +type Float64 float64 + +func (f *Float64) UnmarshalValue(value interface{}) error { + if v, ok := value.(*big.Rat); ok { + f64, _ := v.Float64() + *f = Float64(f64) + } + return nil +} + +func Test_Issue1607(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + type Demo struct { + B Float64 + } + rat := &big.Rat{} + rat.SetFloat64(1.5) + + var demos = make([]Demo, 1) + err := gconv.Scan([]map[string]interface{}{ + {"A": 1, "B": rat}, + }, &demos) + t.AssertNil(err) + t.Assert(demos[0].B, 1.5) + }) +} diff --git a/util/gconv/gconv_z_unit_slice_test.go b/util/gconv/gconv_z_unit_slice_test.go index 750711daa..7ce358a4f 100644 --- a/util/gconv/gconv_z_unit_slice_test.go +++ b/util/gconv/gconv_z_unit_slice_test.go @@ -103,6 +103,10 @@ func Test_Strings(t *testing.T) { } t.AssertEQ(gconv.Strings(array), []string{"1", "2", "3"}) }) + // https://github.com/gogf/gf/issues/1750 + gtest.C(t, func(t *gtest.T) { + t.AssertEQ(gconv.Strings("123"), []string{"123"}) + }) } func Test_Slice_Interfaces(t *testing.T) { @@ -163,7 +167,7 @@ func Test_Slice_Structs(t *testing.T) { {"id": 2, "name": "smith", "age": 20}, } err := gconv.Structs(params, &users) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(len(users), 2) t.Assert(users[0].Id, params[0]["id"]) t.Assert(users[0].Name, params[0]["name"]) diff --git a/util/gconv/gconv_z_unit_struct_marshal_unmarshal_test.go b/util/gconv/gconv_z_unit_struct_marshal_unmarshal_test.go index c6381923b..b200e732b 100644 --- a/util/gconv/gconv_z_unit_struct_marshal_unmarshal_test.go +++ b/util/gconv/gconv_z_unit_struct_marshal_unmarshal_test.go @@ -40,7 +40,7 @@ func Test_Struct_UnmarshalValue1(t *testing.T) { gtest.C(t, func(t *gtest.T) { st := &MyTimeSt{} err := gconv.Struct(g.Map{"ServiceDate": "2020-10-10 12:00:01"}, st) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(st.ServiceDate.Time.Format("2006-01-02 15:04:05"), "2020-10-10 12:00:01") }) gtest.C(t, func(t *gtest.T) { @@ -103,7 +103,7 @@ func Test_Struct_UnmarshalValue2(t *testing.T) { var p1, p2 *Pkg p1 = NewPkg([]byte("123")) err := gconv.Struct(p1.Marshal(), &p2) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(p1, p2) }) } diff --git a/util/gconv/gconv_z_unit_struct_test.go b/util/gconv/gconv_z_unit_struct_test.go index 75e8adbb7..ea9c29e9a 100644 --- a/util/gconv/gconv_z_unit_struct_test.go +++ b/util/gconv/gconv_z_unit_struct_test.go @@ -11,6 +11,7 @@ import ( "time" "github.com/gogf/gf/v2/container/gvar" + "github.com/gogf/gf/v2/encoding/gjson" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/internal/json" "github.com/gogf/gf/v2/os/gtime" @@ -340,7 +341,7 @@ func Test_Struct_Attr_CustomType1(t *testing.T) { gtest.C(t, func(t *gtest.T) { user := new(User) err := gconv.Struct(g.Map{"id": 1, "name": "john"}, user) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(user.Id, 1) t.Assert(user.Name, "john") }) @@ -355,7 +356,7 @@ func Test_Struct_Attr_CustomType2(t *testing.T) { gtest.C(t, func(t *gtest.T) { user := new(User) err := gconv.Struct(g.Map{"id": g.Slice{1, 2}, "name": "john"}, user) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(user.Id, g.Slice{1, 2}) t.Assert(user.Name, "john") }) @@ -408,7 +409,7 @@ func Test_Struct_PrivateAttribute(t *testing.T) { gtest.C(t, func(t *gtest.T) { user := new(User) err := gconv.Struct(g.Map{"id": 1, "name": "john"}, user) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(user.Id, 1) t.Assert(user.name, "") }) @@ -431,7 +432,7 @@ func Test_StructEmbedded1(t *testing.T) { "age": 18, } err := gconv.Struct(params, user) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(user.Id, params["id"]) t.Assert(user.Name, params["name"]) t.Assert(user.Age, 18) @@ -459,7 +460,7 @@ func Test_StructEmbedded2(t *testing.T) { gtest.C(t, func(t *gtest.T) { user := new(User) err := gconv.Struct(params, user) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(user.Id, 1) t.Assert(user.Uid, 10) t.Assert(user.Name, "john") @@ -492,7 +493,7 @@ func Test_StructEmbedded3(t *testing.T) { } user := new(User) err := gconv.Struct(data, user) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(user.Id, 100) t.Assert(user.Uid, 101) t.Assert(user.Nickname, "T1") @@ -564,16 +565,16 @@ func Test_StructEmbedded5(t *testing.T) { user1 := new(UserWithBase1) user2 := new(UserWithBase2) err = gconv.Struct(data, user1) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(user1, &UserWithBase1{1, "john", Base{"123", "456"}}) err = gconv.Struct(data, user2) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(user2, &UserWithBase2{1, "john", Base{"", ""}}) var user3 *UserWithBase1 err = gconv.Struct(user1, &user3) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(user3, user1) }) } @@ -669,7 +670,7 @@ func Test_Struct_Create(t *testing.T) { "Name": "john", } err := gconv.Struct(params, &user) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(user.Uid, 1) t.Assert(user.Name, "john") }) @@ -702,7 +703,7 @@ func Test_Struct_Interface(t *testing.T) { "Name": nil, } err := gconv.Struct(params, &user) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(user.Uid, 1) t.Assert(user.Name, nil) }) @@ -731,7 +732,7 @@ func Test_Struct_NilAttribute(t *testing.T) { "txt": "hello", "items": nil, }, m) - t.Assert(err, nil) + t.AssertNil(err) t.AssertNE(m.Me, nil) t.Assert(m.Me["day"], "20009") t.Assert(m.Items, nil) @@ -874,11 +875,11 @@ func Test_Struct_Complex(t *testing.T) { }` m := make(g.Map) err := json.UnmarshalUseNumber([]byte(data), &m) - t.Assert(err, nil) + t.AssertNil(err) model := new(XinYanModel) err = gconv.Struct(m, model) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(model.ErrorCode, nil) t.Assert(model.ErrorMsg, nil) t.Assert(model.Success, true) @@ -933,7 +934,7 @@ func Test_Struct_Embedded(t *testing.T) { } v2 := g.Map{} err := gconv.Struct(v2, &v1) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(v1.Test(), "john") }) // Implemented interface attribute. @@ -945,7 +946,7 @@ func Test_Struct_Embedded(t *testing.T) { "name": "test", } err := gconv.Struct(v2, &v1) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(v1.Test(), "test") }) // No implemented interface attribute. @@ -955,7 +956,7 @@ func Test_Struct_Embedded(t *testing.T) { "name": "test", } err := gconv.Struct(v2, &v1) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(v1.TestInterface, nil) }) } @@ -968,7 +969,7 @@ func Test_Struct_Slice(t *testing.T) { user := new(User) array := g.Slice{1, 2, 3} err := gconv.Struct(g.Map{"scores": array}, user) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(user.Scores, array) }) gtest.C(t, func(t *gtest.T) { @@ -978,7 +979,7 @@ func Test_Struct_Slice(t *testing.T) { user := new(User) array := g.Slice{1, 2, 3} err := gconv.Struct(g.Map{"scores": array}, user) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(user.Scores, array) }) gtest.C(t, func(t *gtest.T) { @@ -988,7 +989,7 @@ func Test_Struct_Slice(t *testing.T) { user := new(User) array := g.Slice{1, 2, 3} err := gconv.Struct(g.Map{"scores": array}, user) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(user.Scores, array) }) gtest.C(t, func(t *gtest.T) { @@ -998,7 +999,7 @@ func Test_Struct_Slice(t *testing.T) { user := new(User) array := g.Slice{1, 2, 3} err := gconv.Struct(g.Map{"scores": array}, user) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(user.Scores, array) }) gtest.C(t, func(t *gtest.T) { @@ -1008,7 +1009,7 @@ func Test_Struct_Slice(t *testing.T) { user := new(User) array := g.Slice{1, 2, 3} err := gconv.Struct(g.Map{"scores": array}, user) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(user.Scores, array) }) gtest.C(t, func(t *gtest.T) { @@ -1018,7 +1019,7 @@ func Test_Struct_Slice(t *testing.T) { user := new(User) array := g.Slice{1, 2, 3} err := gconv.Struct(g.Map{"scores": array}, user) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(user.Scores, array) }) gtest.C(t, func(t *gtest.T) { @@ -1028,7 +1029,7 @@ func Test_Struct_Slice(t *testing.T) { user := new(User) array := g.Slice{1, 2, 3} err := gconv.Struct(g.Map{"scores": array}, user) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(user.Scores, array) }) gtest.C(t, func(t *gtest.T) { @@ -1038,7 +1039,7 @@ func Test_Struct_Slice(t *testing.T) { user := new(User) array := g.Slice{1, 2, 3} err := gconv.Struct(g.Map{"scores": array}, user) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(user.Scores, array) }) } @@ -1078,7 +1079,7 @@ func Test_Struct_WithJson(t *testing.T) { b, _ := json.Marshal(b1) b2 := &B{} err := gconv.Struct(b, b2) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(b2, b1) }) } @@ -1102,7 +1103,7 @@ func Test_Struct_AttrStructHasTheSameTag(t *testing.T) { } order := new(Order) err := gconv.Struct(data, order) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(order.Id, data["id"]) t.Assert(order.UpdatedAt, data["updated_at"]) t.Assert(order.Product.Id, 0) @@ -1126,7 +1127,7 @@ func Test_Struct_DirectReflectSet(t *testing.T) { b *A ) err := gconv.Struct(a, &b) - t.Assert(err, nil) + t.AssertNil(err) t.AssertEQ(a, b) }) gtest.C(t, func(t *gtest.T) { @@ -1138,7 +1139,7 @@ func Test_Struct_DirectReflectSet(t *testing.T) { b A ) err := gconv.Struct(a, &b) - t.Assert(err, nil) + t.AssertNil(err) t.AssertEQ(a, b) }) } @@ -1160,7 +1161,7 @@ func Test_Struct_NilEmbeddedStructAttribute(t *testing.T) { "id": 1, "name": nil, }, &b) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(b.Id, 1) t.Assert(b.Name, "") }) @@ -1175,7 +1176,7 @@ func Test_Struct_JsonParam(t *testing.T) { gtest.C(t, func(t *gtest.T) { var a = A{} err := gconv.Struct([]byte(`{"id":1,"name":"john"}`), &a) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(a.Id, 1) t.Assert(a.Name, "john") }) @@ -1183,7 +1184,7 @@ func Test_Struct_JsonParam(t *testing.T) { gtest.C(t, func(t *gtest.T) { var a = &A{} err := gconv.Struct([]byte(`{"id":1,"name":"john"}`), a) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(a.Id, 1) t.Assert(a.Name, "john") }) @@ -1191,7 +1192,7 @@ func Test_Struct_JsonParam(t *testing.T) { gtest.C(t, func(t *gtest.T) { var a *A err := gconv.Struct([]byte(`{"id":1,"name":"john"}`), &a) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(a.Id, 1) t.Assert(a.Name, "john") }) @@ -1213,7 +1214,7 @@ func Test_Struct_GVarAttribute(t *testing.T) { } ) err := gconv.Struct(data, &a) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(a.Id, data["id"]) t.Assert(a.Name, data["name"]) t.Assert(a.Status, data["status"]) @@ -1258,3 +1259,65 @@ func Test_Struct_Empty_MapStringString(t *testing.T) { t.AssertNil(err) }) } + +// https://github.com/gogf/gf/issues/1563 +func Test_Struct_Issue1563(t *testing.T) { + type User struct { + Pass1 string `c:"password1"` + } + gtest.C(t, func(t *gtest.T) { + for i := 0; i < 100; i++ { + user := new(User) + params2 := g.Map{ + "password1": "111", + //"PASS1": "222", + "Pass1": "333", + } + if err := gconv.Struct(params2, user); err == nil { + t.Assert(user.Pass1, `111`) + } + } + }) +} + +// https://github.com/gogf/gf/issues/1597 +func Test_Struct_Issue1597(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + type S struct { + A int + B json.RawMessage + } + + jsonByte := []byte(`{ + "a":1, + "b":{ + "c": 3 + } + }`) + data, err := gjson.DecodeToJson(jsonByte) + t.AssertNil(err) + s := &S{} + err = data.Scan(s) + t.AssertNil(err) + t.Assert(s.B, `{"c":3}`) + }) +} + +func Test_Scan_WithDoubleSliceAttribute(t *testing.T) { + inputData := [][]string{ + {"aa", "bb", "cc"}, + {"11", "22", "33"}, + } + data := struct { + Data [][]string + }{ + Data: inputData, + } + gtest.C(t, func(t *gtest.T) { + jv := gjson.New(gjson.MustEncodeString(data)) + err := jv.Scan(&data) + t.AssertNil(err) + t.Assert(data.Data, inputData) + }) + +} diff --git a/util/gconv/gconv_z_unit_structs_test.go b/util/gconv/gconv_z_unit_structs_test.go index f834fbb6b..673379b07 100644 --- a/util/gconv/gconv_z_unit_structs_test.go +++ b/util/gconv/gconv_z_unit_structs_test.go @@ -32,7 +32,7 @@ func Test_Structs_WithTag(t *testing.T) { }, } err := gconv.Structs(params, &users) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(len(users), 2) t.Assert(users[0].Uid, 1) t.Assert(users[0].NickName, "name1") @@ -52,7 +52,7 @@ func Test_Structs_WithTag(t *testing.T) { }, } err := gconv.Structs(params, &users) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(len(users), 2) t.Assert(users[0].Uid, 1) t.Assert(users[0].NickName, "name1") @@ -79,7 +79,7 @@ func Test_Structs_WithoutTag(t *testing.T) { }, } err := gconv.Structs(params, &users) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(len(users), 2) t.Assert(users[0].Uid, 1) t.Assert(users[0].NickName, "name1") @@ -99,7 +99,7 @@ func Test_Structs_WithoutTag(t *testing.T) { }, } err := gconv.Structs(params, &users) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(len(users), 2) t.Assert(users[0].Uid, 1) t.Assert(users[0].NickName, "name1") @@ -166,7 +166,7 @@ func Test_Structs_DirectReflectSet(t *testing.T) { b []*A ) err := gconv.Structs(a, &b) - t.Assert(err, nil) + t.AssertNil(err) t.AssertEQ(a, b) }) gtest.C(t, func(t *gtest.T) { @@ -178,7 +178,7 @@ func Test_Structs_DirectReflectSet(t *testing.T) { b []A ) err := gconv.Structs(a, &b) - t.Assert(err, nil) + t.AssertNil(err) t.AssertEQ(a, b) }) } @@ -199,7 +199,7 @@ func Test_Structs_IntSliceAttribute(t *testing.T) { g.Map{"id": nil, "name": "john"}, g.Map{"id": nil, "name": "smith"}, }, &array) - t.Assert(err, nil) + t.AssertNil(err) t.Assert(len(array), 2) t.Assert(array[0].Name, "john") t.Assert(array[1].Name, "smith") diff --git a/util/grand/grand.go b/util/grand/grand.go index 77478cee4..34554a2f5 100644 --- a/util/grand/grand.go +++ b/util/grand/grand.go @@ -60,16 +60,12 @@ func N(min, max int) int { return min } if min >= 0 { - // Because Intn dose not support negative number, - // so we should first shift the value to left, - // then call Intn to produce the random number, - // and finally shift the result back to right. - return Intn(max-(min-0)+1) + (min - 0) + return Intn(max-min+1) + min } if min < 0 { - // Because Intn dose not support negative number, + // As `Intn` dose not support negative number, // so we should first shift the value to right, - // then call Intn to produce the random number, + // then call `Intn` to produce the random number, // and finally shift the result back to left. return Intn(max+(0-min)+1) - (0 - min) } diff --git a/util/gtag/gtag.go b/util/gtag/gtag.go index 68d915806..058159386 100644 --- a/util/gtag/gtag.go +++ b/util/gtag/gtag.go @@ -11,8 +11,9 @@ package gtag import ( - "fmt" "regexp" + + "github.com/gogf/gf/v2/errors/gerror" ) var ( @@ -21,20 +22,30 @@ var ( ) // Set sets tag content for specified name. +// Note that it panics if `name` already exists. func Set(name, value string) { if _, ok := data[name]; ok { - panic(fmt.Sprintf(`value for tag "%s" already exists`, name)) + panic(gerror.Newf(`value for tag name "%s" already exists`, name)) } data[name] = value } +// SetOver performs as Set, but it overwrites the old value if `name` already exists. +func SetOver(name, value string) { + data[name] = value +} + // Sets sets multiple tag content by map. func Sets(m map[string]string) { for k, v := range m { - if _, ok := data[k]; ok { - panic(fmt.Sprintf(`value for tag "%s" already exists`, k)) - } - data[k] = v + Set(k, v) + } +} + +// SetsOver performs as Sets, but it overwrites the old value if `name` already exists. +func SetsOver(m map[string]string) { + for k, v := range m { + SetOver(k, v) } } @@ -46,8 +57,8 @@ func Get(name string) string { // Parse parses and returns the content by replacing all tag name variable to // its content for given `content`. // Eg: -// If "Demo:content" in tag mapping, -// Parse(`This is {Demo}`) -> `This is content`. +// gtag.Set("demo", "content") +// Parse(`This is {demo}`) -> `This is content`. func Parse(content string) string { return regex.ReplaceAllStringFunc(content, func(s string) string { if v, ok := data[s[1:len(s)-1]]; ok { diff --git a/util/gtag/gtag_z_unit_test.go b/util/gtag/gtag_z_unit_test.go index d38c9cb81..a8e8160c4 100644 --- a/util/gtag/gtag_z_unit_test.go +++ b/util/gtag/gtag_z_unit_test.go @@ -11,26 +11,56 @@ import ( "testing" "github.com/gogf/gf/v2/frame/g" - "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/test/gtest" "github.com/gogf/gf/v2/util/gtag" + "github.com/gogf/gf/v2/util/guid" ) func Test_Set_Get(t *testing.T) { gtest.C(t, func(t *gtest.T) { - k := gtime.TimestampNanoStr() - v := gtime.TimestampNanoStr() + k := guid.S() + v := guid.S() gtag.Set(k, v) t.Assert(gtag.Get(k), v) }) } +func Test_SetOver_Get(t *testing.T) { + // panic by Set + gtest.C(t, func(t *gtest.T) { + var ( + k = guid.S() + v1 = guid.S() + v2 = guid.S() + ) + gtag.Set(k, v1) + t.Assert(gtag.Get(k), v1) + defer func() { + t.AssertNE(recover(), nil) + }() + gtag.Set(k, v2) + }) + gtest.C(t, func(t *gtest.T) { + var ( + k = guid.S() + v1 = guid.S() + v2 = guid.S() + ) + gtag.SetOver(k, v1) + t.Assert(gtag.Get(k), v1) + gtag.SetOver(k, v2) + t.Assert(gtag.Get(k), v2) + }) +} + func Test_Sets_Get(t *testing.T) { gtest.C(t, func(t *gtest.T) { - k1 := gtime.TimestampNanoStr() - k2 := gtime.TimestampNanoStr() - v1 := gtime.TimestampNanoStr() - v2 := gtime.TimestampNanoStr() + var ( + k1 = guid.S() + k2 = guid.S() + v1 = guid.S() + v2 = guid.S() + ) gtag.Sets(g.MapStrStr{ k1: v1, k2: v2, @@ -40,13 +70,60 @@ func Test_Sets_Get(t *testing.T) { }) } +func Test_SetsOver_Get(t *testing.T) { + // panic by Sets + gtest.C(t, func(t *gtest.T) { + var ( + k1 = guid.S() + k2 = guid.S() + v1 = guid.S() + v2 = guid.S() + v3 = guid.S() + ) + gtag.Sets(g.MapStrStr{ + k1: v1, + k2: v2, + }) + t.Assert(gtag.Get(k1), v1) + t.Assert(gtag.Get(k2), v2) + defer func() { + t.AssertNE(recover(), nil) + }() + gtag.Sets(g.MapStrStr{ + k1: v3, + k2: v3, + }) + }) + gtest.C(t, func(t *gtest.T) { + var ( + k1 = guid.S() + k2 = guid.S() + v1 = guid.S() + v2 = guid.S() + v3 = guid.S() + ) + gtag.SetsOver(g.MapStrStr{ + k1: v1, + k2: v2, + }) + t.Assert(gtag.Get(k1), v1) + t.Assert(gtag.Get(k2), v2) + gtag.SetsOver(g.MapStrStr{ + k1: v3, + k2: v3, + }) + t.Assert(gtag.Get(k1), v3) + t.Assert(gtag.Get(k2), v3) + }) +} + func Test_Parse(t *testing.T) { gtest.C(t, func(t *gtest.T) { var ( - k1 = gtime.TimestampNanoStr() - k2 = gtime.TimestampNanoStr() - v1 = gtime.TimestampNanoStr() - v2 = gtime.TimestampNanoStr() + k1 = guid.S() + k2 = guid.S() + v1 = guid.S() + v2 = guid.S() content = fmt.Sprintf(`this is {%s} and {%s}`, k1, k2) expect = fmt.Sprintf(`this is %s and %s`, v1, v2) ) diff --git a/util/guid/guid.go b/util/guid/guid.go index 2259abb81..c217ed455 100644 --- a/util/guid/guid.go +++ b/util/guid/guid.go @@ -13,6 +13,8 @@ import ( "github.com/gogf/gf/v2/container/gtype" "github.com/gogf/gf/v2/encoding/ghash" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/net/gipv4" "github.com/gogf/gf/v2/util/grand" ) @@ -38,7 +40,7 @@ func init() { macAddrBytes = append(macAddrBytes, []byte(mac)...) } b := []byte{'0', '0', '0', '0', '0', '0', '0'} - s := strconv.FormatUint(uint64(ghash.DJBHash(macAddrBytes)), 36) + s := strconv.FormatUint(uint64(ghash.SDBM(macAddrBytes)), 36) copy(b, s) macAddrStr = string(b) } @@ -61,13 +63,13 @@ func init() { // be token by random string. // // The returned string is composed with: -// 1. Default: MAC(7) + PID(4) + TimestampNano(12) + Sequence(3) + RandomString(6) -// 2. CustomData: Data(7/14) + TimestampNano(12) + Sequence(3) + RandomString(3/10) +// 1. Default: MACHash(7) + PID(4) + TimestampNano(12) + Sequence(3) + RandomString(6) +// 2. CustomData: DataHash(7/14) + TimestampNano(12) + Sequence(3) + RandomString(3/10) // // Note that: // 1. The returned length is fixed to 32 bytes for performance purpose. // 2. The custom parameter `data` composed should have unique attribute in your -// business situation. +// business scenario. func S(data ...[]byte) string { var ( b = make([]byte, 32) @@ -92,7 +94,10 @@ func S(data ...[]byte) string { copy(b[n+12:], getSequence()) copy(b[n+12+3:], getRandomStr(32-n-12-3)) } else { - panic("too many data parts, it should be no more than 2 parts") + panic(gerror.NewCode( + gcode.CodeInvalidParameter, + "too many data parts, it should be no more than 2 parts", + )) } return string(b) } @@ -124,7 +129,7 @@ func getRandomStr(n int) []byte { // getDataHashStr creates and returns hash bytes in 7 bytes with given data bytes. func getDataHashStr(data []byte) []byte { b := []byte{'0', '0', '0', '0', '0', '0', '0'} - s := strconv.FormatUint(uint64(ghash.DJBHash(data)), 36) + s := strconv.FormatUint(uint64(ghash.SDBM(data)), 36) copy(b, s) return b } diff --git a/util/gutil/gutil_dump.go b/util/gutil/gutil_dump.go index f22ab31a3..148824b40 100644 --- a/util/gutil/gutil_dump.go +++ b/util/gutil/gutil_dump.go @@ -13,6 +13,7 @@ import ( "reflect" "strings" + "github.com/gogf/gf/v2/internal/reflection" "github.com/gogf/gf/v2/os/gstructs" "github.com/gogf/gf/v2/text/gstr" ) @@ -34,14 +35,16 @@ type iMarshalJSON interface { // DumpOption specifies the behavior of function Export. type DumpOption struct { - WithType bool // WithType specifies dumping content with type information. + WithType bool // WithType specifies dumping content with type information. + ExportedOnly bool // Only dump Exported fields for structs. } // Dump prints variables `values` to stdout with more manually readable. func Dump(values ...interface{}) { for _, value := range values { DumpWithOption(value, DumpOption{ - WithType: false, + WithType: false, + ExportedOnly: false, }) } } @@ -51,7 +54,8 @@ func Dump(values ...interface{}) { func DumpWithType(values ...interface{}) { for _, value := range values { DumpWithOption(value, DumpOption{ - WithType: true, + WithType: true, + ExportedOnly: false, }) } } @@ -60,7 +64,8 @@ func DumpWithType(values ...interface{}) { func DumpWithOption(value interface{}, option DumpOption) { buffer := bytes.NewBuffer(nil) DumpTo(buffer, value, DumpOption{ - WithType: option.WithType, + WithType: option.WithType, + ExportedOnly: option.ExportedOnly, }) fmt.Println(buffer.String()) } @@ -69,24 +74,43 @@ func DumpWithOption(value interface{}, option DumpOption) { func DumpTo(writer io.Writer, value interface{}, option DumpOption) { buffer := bytes.NewBuffer(nil) doDump(value, "", buffer, doDumpOption{ - WithType: option.WithType, + WithType: option.WithType, + ExportedOnly: option.ExportedOnly, }) _, _ = writer.Write(buffer.Bytes()) } type doDumpOption struct { - WithType bool + WithType bool + ExportedOnly bool } func doDump(value interface{}, indent string, buffer *bytes.Buffer, option doDumpOption) { + if value == nil { + buffer.WriteString(``) + return + } + var reflectValue reflect.Value + if v, ok := value.(reflect.Value); ok { + reflectValue = v + if v.IsValid() && v.CanInterface() { + value = v.Interface() + } else { + if convertedValue, ok := reflection.ValueToInterface(v); ok { + value = convertedValue + } + } + } else { + reflectValue = reflect.ValueOf(value) + } + // Double check nil value. if value == nil { buffer.WriteString(``) return } var ( - reflectValue = reflect.ValueOf(value) reflectKind = reflectValue.Kind() - reflectTypeName = reflect.TypeOf(value).String() + reflectTypeName = reflectValue.Type().String() newIndent = indent + dumpIndent ) reflectTypeName = strings.ReplaceAll(reflectTypeName, `[]uint8`, `[]byte`) @@ -106,6 +130,7 @@ func doDump(value interface{}, indent string, buffer *bytes.Buffer, option doDum Option: option, ReflectValue: reflectValue, ReflectTypeName: reflectTypeName, + ExportedOnly: option.ExportedOnly, } ) switch reflectKind { @@ -142,15 +167,18 @@ func doDump(value interface{}, indent string, buffer *bytes.Buffer, option doDum doDumpNumber(exportInternalInput) case reflect.Chan: - buffer.WriteString(fmt.Sprintf(`<%s>`, reflect.TypeOf(value).String())) + buffer.WriteString(fmt.Sprintf(`<%s>`, reflectValue.Type().String())) case reflect.Func: if reflectValue.IsNil() || !reflectValue.IsValid() { buffer.WriteString(``) } else { - buffer.WriteString(fmt.Sprintf(`<%s>`, reflect.TypeOf(value).String())) + buffer.WriteString(fmt.Sprintf(`<%s>`, reflectValue.Type().String())) } + case reflect.Interface: + doDump(exportInternalInput.ReflectValue.Elem(), indent, buffer, option) + default: doDumpDefault(exportInternalInput) } @@ -164,6 +192,7 @@ type doDumpInternalInput struct { Option doDumpOption ReflectValue reflect.Value ReflectTypeName string + ExportedOnly bool } func doDumpSlice(in doDumpInternalInput) { @@ -195,16 +224,21 @@ func doDumpSlice(in doDumpInternalInput) { } for i := 0; i < in.ReflectValue.Len(); i++ { in.Buffer.WriteString(in.NewIndent) - doDump(in.ReflectValue.Index(i).Interface(), in.NewIndent, in.Buffer, in.Option) + doDump(in.ReflectValue.Index(i), in.NewIndent, in.Buffer, in.Option) in.Buffer.WriteString(",\n") } in.Buffer.WriteString(fmt.Sprintf("%s]", in.Indent)) } func doDumpMap(in doDumpInternalInput) { - var ( - mapKeys = in.ReflectValue.MapKeys() - ) + var mapKeys = make([]reflect.Value, 0) + for _, key := range in.ReflectValue.MapKeys() { + if !key.CanInterface() { + continue + } + mapKey := key + mapKeys = append(mapKeys, mapKey) + } if len(mapKeys) == 0 { if !in.Option.WithType { in.Buffer.WriteString("{}") @@ -254,7 +288,7 @@ func doDumpMap(in doDumpInternalInput) { )) } // Map value dump. - doDump(in.ReflectValue.MapIndex(mapKey).Interface(), in.NewIndent, in.Buffer, in.Option) + doDump(in.ReflectValue.MapIndex(mapKey), in.NewIndent, in.Buffer, in.Option) in.Buffer.WriteString(",\n") } in.Buffer.WriteString(fmt.Sprintf("%s}", in.Indent)) @@ -265,7 +299,17 @@ func doDumpStruct(in doDumpInternalInput) { Pointer: in.Value, RecursiveOption: gstructs.RecursiveOptionEmbedded, }) - if len(structFields) == 0 { + var ( + hasNoExportedFields = true + _, isReflectValue = in.Value.(reflect.Value) + ) + for _, field := range structFields { + if field.IsExported() { + hasNoExportedFields = false + break + } + } + if !isReflectValue && (len(structFields) == 0 || hasNoExportedFields) { var ( structContentStr = "" attributeCountStr = "0" @@ -277,6 +321,11 @@ func doDumpStruct(in doDumpInternalInput) { } else if v, ok := in.Value.(iMarshalJSON); ok { b, _ := v.MarshalJSON() structContentStr = string(b) + } else { + // Has no common interface implements. + if len(structFields) != 0 { + goto dumpStructFields + } } if structContentStr == "" { structContentStr = "{}" @@ -296,11 +345,16 @@ func doDumpStruct(in doDumpInternalInput) { } return } + +dumpStructFields: var ( maxSpaceNum = 0 tmpSpaceNum = 0 ) for _, field := range structFields { + if in.ExportedOnly && !field.IsExported() { + continue + } tmpSpaceNum = len(field.Name()) if tmpSpaceNum > maxSpaceNum { maxSpaceNum = tmpSpaceNum @@ -312,6 +366,9 @@ func doDumpStruct(in doDumpInternalInput) { in.Buffer.WriteString(fmt.Sprintf("%s(%d) {\n", in.ReflectTypeName, len(structFields))) } for _, field := range structFields { + if in.ExportedOnly && !field.IsExported() { + continue + } tmpSpaceNum = len(fmt.Sprintf(`%v`, field.Name())) in.Buffer.WriteString(fmt.Sprintf( "%s%s:%s", @@ -319,7 +376,7 @@ func doDumpStruct(in doDumpInternalInput) { field.Name(), strings.Repeat(" ", maxSpaceNum-tmpSpaceNum+1), )) - doDump(field.Value.Interface(), in.NewIndent, in.Buffer, in.Option) + doDump(field.Value, in.NewIndent, in.Buffer, in.Option) in.Buffer.WriteString(",\n") } in.Buffer.WriteString(fmt.Sprintf("%s}", in.Indent)) @@ -371,7 +428,13 @@ func doDumpBool(in doDumpInternalInput) { } func doDumpDefault(in doDumpInternalInput) { - s := fmt.Sprintf("%v", in.Value) + var s string + if in.ReflectValue.IsValid() && in.ReflectValue.CanInterface() { + s = fmt.Sprintf("%v", in.ReflectValue.Interface()) + } + if s == "" { + s = fmt.Sprintf("%v", in.Value) + } s = gstr.Trim(s, `<>`) if !in.Option.WithType { in.Buffer.WriteString(s) diff --git a/util/gutil/gutil_map.go b/util/gutil/gutil_map.go index 8be8af5a6..92e470434 100644 --- a/util/gutil/gutil_map.go +++ b/util/gutil/gutil_map.go @@ -76,10 +76,7 @@ func MapPossibleItemByKey(data map[string]interface{}, key string) (foundKey str // // Note that this function might be of low performance. func MapContainsPossibleKey(data map[string]interface{}, key string) bool { - if k, _ := MapPossibleItemByKey(data, key); k != "" { - return true - } - return false + return utils.MapContainsPossibleKey(data, key) } // MapOmitEmpty deletes all empty values from given map. diff --git a/util/gutil/gutil_z_unit_dump_test.go b/util/gutil/gutil_z_unit_dump_test.go index d90052643..336e74fb4 100755 --- a/util/gutil/gutil_z_unit_dump_test.go +++ b/util/gutil/gutil_z_unit_dump_test.go @@ -7,8 +7,10 @@ package gutil_test import ( + "bytes" "testing" + "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/test/gtest" @@ -71,6 +73,23 @@ func Test_Dump(t *testing.T) { }) } +func Test_Dump_Map(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + buffer := bytes.NewBuffer(nil) + m := g.Map{ + "k1": g.Map{ + "k2": "v2", + }, + } + gutil.DumpTo(buffer, m, gutil.DumpOption{}) + t.Assert(buffer.String(), `{ + "k1": { + "k2": "v2", + }, +}`) + }) +} + func TestDumpWithType(t *testing.T) { type CommonReq struct { AppId int64 `json:"appId" v:"required" in:"path" des:"应用Id" sum:"应用Id Summary"` @@ -142,3 +161,109 @@ func Test_Dump_Slashes(t *testing.T) { gutil.DumpWithType(req.Content) }) } + +// https://github.com/gogf/gf/issues/1661 +func Test_Dump_Issue1661(t *testing.T) { + type B struct { + ba int + bb string + } + type A struct { + aa int + ab string + cc []B + } + gtest.C(t, func(t *gtest.T) { + var q1 []A + var q2 []A + q2 = make([]A, 0) + q1 = []A{{aa: 1, ab: "1", cc: []B{{ba: 1}, {ba: 2}, {ba: 3}}}, {aa: 2, ab: "2", cc: []B{{ba: 1}, {ba: 2}, {ba: 3}}}} + for _, q1v := range q1 { + x := []string{"11", "22"} + for _, iv2 := range x { + ls := q1v + for i, _ := range ls.cc { + sj := iv2 + ls.cc[i].bb = sj + } + q2 = append(q2, ls) + } + } + buffer := bytes.NewBuffer(nil) + gutil.DumpTo(buffer, q2, gutil.DumpOption{}) + t.Assert(buffer.String(), `[ + { + aa: 1, + ab: "1", + cc: [ + { + ba: 1, + bb: "22", + }, + { + ba: 2, + bb: "22", + }, + { + ba: 3, + bb: "22", + }, + ], + }, + { + aa: 1, + ab: "1", + cc: [ + { + ba: 1, + bb: "22", + }, + { + ba: 2, + bb: "22", + }, + { + ba: 3, + bb: "22", + }, + ], + }, + { + aa: 2, + ab: "2", + cc: [ + { + ba: 1, + bb: "22", + }, + { + ba: 2, + bb: "22", + }, + { + ba: 3, + bb: "22", + }, + ], + }, + { + aa: 2, + ab: "2", + cc: [ + { + ba: 1, + bb: "22", + }, + { + ba: 2, + bb: "22", + }, + { + ba: 3, + bb: "22", + }, + ], + }, +]`) + }) +} diff --git a/util/gvalid/gvalid.go b/util/gvalid/gvalid.go index e13b1e5ac..80373104f 100644 --- a/util/gvalid/gvalid.go +++ b/util/gvalid/gvalid.go @@ -8,9 +8,12 @@ package gvalid import ( + "context" + "reflect" "regexp" "strings" + "github.com/gogf/gf/v2/internal/intlog" "github.com/gogf/gf/v2/text/gregex" ) @@ -20,8 +23,10 @@ type CustomMsg = map[string]interface{} // fieldRule defined the alias name and rule string for specified field. type fieldRule struct { - Name string // Alias name for the field. - Rule string // Rule string like: "max:6" + Name string // Alias name for the field. + Rule string // Rule string like: "max:6" + IsMeta bool // Is this rule is from gmeta.Meta, which marks it as whole struct rule. + FieldKind reflect.Kind // Kind of struct field, which is used for parameter type checks. } // iNoValidation is an interface that marks current struct not validated by package `gvalid`. @@ -40,6 +45,8 @@ const ( noValidationTagName = "nv" // no validation tag name for struct attribute. ruleNameBail = "bail" // the name for rule "bail" ruleNameCi = "ci" // the name for rule "ci" + emptyJsonArrayStr = "[]" // Empty json string for array type. + emptyJsonObjectStr = "{}" // Empty json string for object type. ) var ( @@ -197,11 +204,23 @@ var ( } ) -// parseSequenceTag parses one sequence tag to field, rule and error message. +// ParseTagValue parses one sequence tag to field, rule and error message. // The sequence tag is like: [alias@]rule[...#msg...] -func parseSequenceTag(tag string) (field, rule, msg string) { +func ParseTagValue(tag string) (field, rule, msg string) { // Complete sequence tag. // Example: name@required|length:2,20|password3|same:password1#||密码强度不足|两次密码不一致 match, _ := gregex.MatchString(`\s*((\w+)\s*@){0,1}\s*([^#]+)\s*(#\s*(.*)){0,1}\s*`, tag) - return strings.TrimSpace(match[2]), strings.TrimSpace(match[3]), strings.TrimSpace(match[5]) + if len(match) > 5 { + msg = strings.TrimSpace(match[5]) + rule = strings.TrimSpace(match[3]) + field = strings.TrimSpace(match[2]) + } else { + intlog.Errorf(context.TODO(), `invalid validation tag value: %s`, tag) + } + return +} + +// GetTags returns the validation tags. +func GetTags() []string { + return structTagPriority } diff --git a/util/gvalid/gvalid_custom_rule.go b/util/gvalid/gvalid_custom_rule.go index 73dc0e648..2521eb4bf 100644 --- a/util/gvalid/gvalid_custom_rule.go +++ b/util/gvalid/gvalid_custom_rule.go @@ -8,8 +8,12 @@ package gvalid import ( "context" + "fmt" + "reflect" + "runtime" "github.com/gogf/gf/v2/container/gvar" + "github.com/gogf/gf/v2/internal/intlog" ) // RuleFunc is the custom function for data validation. @@ -39,6 +43,14 @@ var ( // RegisterRule registers custom validation rule and function for package. func RegisterRule(rule string, f RuleFunc) { + if customRuleFuncMap[rule] != nil { + intlog.PrintFunc(context.TODO(), func() string { + return fmt.Sprintf( + `rule "%s" is overwrotten by function "%s"`, + rule, runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name(), + ) + }) + } customRuleFuncMap[rule] = f } @@ -49,6 +61,18 @@ func RegisterRuleByMap(m map[string]RuleFunc) { } } +// GetRegisteredRuleMap returns all the custom registered rules and associated functions. +func GetRegisteredRuleMap() map[string]RuleFunc { + if len(customRuleFuncMap) == 0 { + return nil + } + ruleMap := make(map[string]RuleFunc) + for k, v := range customRuleFuncMap { + ruleMap[k] = v + } + return ruleMap +} + // DeleteRule deletes custom defined validation one or more rules and associated functions from global package. func DeleteRule(rules ...string) { for _, rule := range rules { diff --git a/util/gvalid/gvalid_error.go b/util/gvalid/gvalid_error.go index ba252d8c6..c489f710e 100644 --- a/util/gvalid/gvalid_error.go +++ b/util/gvalid/gvalid_error.go @@ -9,6 +9,7 @@ package gvalid import ( "strings" + "github.com/gogf/gf/v2/container/gset" "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/text/gstr" @@ -32,7 +33,7 @@ type Error interface { // validationError is the validation error for validation result. type validationError struct { code gcode.Code // Error code. - rules []fieldRule // Rules by sequence, which is used for keeping error sequence. + rules []fieldRule // Rules by sequence, which is used for keeping error sequence only. errors map[string]map[string]error // Error map:map[field]map[rule]message firstKey string // The first error rule key(empty in default). firstItem map[string]error // The first error rule value(nil in default). @@ -52,6 +53,16 @@ func newValidationError(code gcode.Code, rules []fieldRule, fieldRuleErrorMap ma } fieldRuleErrorMap[field] = ruleErrorMap } + // Filter repeated sequence rules. + var ruleNameSet = gset.NewStrSet() + for i := 0; i < len(rules); { + if !ruleNameSet.AddIfNotExist(rules[i].Name) { + // Delete repeated rule. + rules = append(rules[:i], rules[i+1:]...) + continue + } + i++ + } return &validationError{ code: code, rules: rules, diff --git a/util/gvalid/gvalid_validator.go b/util/gvalid/gvalid_validator.go index 80ac39d9a..944774f3c 100644 --- a/util/gvalid/gvalid_validator.go +++ b/util/gvalid/gvalid_validator.go @@ -12,21 +12,22 @@ import ( "reflect" "github.com/gogf/gf/v2/i18n/gi18n" + "github.com/gogf/gf/v2/internal/reflection" "github.com/gogf/gf/v2/internal/utils" "github.com/gogf/gf/v2/util/gconv" ) // Validator is the validation manager for chaining operations. type Validator struct { - i18nManager *gi18n.Manager // I18n manager for error message translation. - data interface{} // Validation data, which can be a map, struct or a certain value to be validated. - assoc interface{} // Associated data, which is usually a map, for union validation. - rules interface{} // Custom validation data. - messages interface{} // Custom validation error messages, which can be string or type of CustomMsg. - ruleFuncMap map[string]RuleFunc // ruleFuncMap stores custom rule functions for current Validator. - useDataInsteadOfObjectAttributes bool // Using `data` as its validation source instead of attribute values from `Object`. - bail bool // Stop validation after the first validation error. - caseInsensitive bool // Case-Insensitive configuration for those rules that need value comparison. + i18nManager *gi18n.Manager // I18n manager for error message translation. + data interface{} // Validation data, which can be a map, struct or a certain value to be validated. + assoc interface{} // Associated data, which is usually a map, for union validation. + rules interface{} // Custom validation data. + messages interface{} // Custom validation error messages, which can be string or type of CustomMsg. + ruleFuncMap map[string]RuleFunc // ruleFuncMap stores custom rule functions for current Validator. + useAssocInsteadOfObjectAttributes bool // Using `assoc` as its validation source instead of attribute values from `Object`. + bail bool // Stop validation after the first validation error. + caseInsensitive bool // Case-Insensitive configuration for those rules that need value comparison. } // New creates and returns a new Validator. @@ -46,7 +47,7 @@ func (v *Validator) Run(ctx context.Context) Error { ) } - originValueAndKind := utils.OriginValueAndKind(v.data) + originValueAndKind := reflection.OriginValueAndKind(v.data) switch originValueAndKind.OriginKind { case reflect.Map: isMapValidation := false @@ -124,14 +125,14 @@ func (v *Validator) Data(data interface{}) *Validator { // Assoc is a chaining operation function, which sets associated validation data for current operation. // The optional parameter `assoc` is usually type of map, which specifies the parameter map used in union validation. -// Calling this function with `assoc` also sets `useDataInsteadOfObjectAttributes` true +// Calling this function with `assoc` also sets `useAssocInsteadOfObjectAttributes` true func (v *Validator) Assoc(assoc interface{}) *Validator { if assoc == nil { return v } newValidator := v.Clone() newValidator.assoc = assoc - newValidator.useDataInsteadOfObjectAttributes = true + newValidator.useAssocInsteadOfObjectAttributes = true return newValidator } diff --git a/util/gvalid/gvalid_validator_check_map.go b/util/gvalid/gvalid_validator_check_map.go index 83b7d4d17..997706751 100644 --- a/util/gvalid/gvalid_validator_check_map.go +++ b/util/gvalid/gvalid_validator_check_map.go @@ -13,7 +13,7 @@ import ( "strings" "github.com/gogf/gf/v2/errors/gcode" - "github.com/gogf/gf/v2/internal/utils" + "github.com/gogf/gf/v2/internal/reflection" "github.com/gogf/gf/v2/util/gconv" ) @@ -31,7 +31,7 @@ func (v *Validator) doCheckMap(ctx context.Context, params interface{}) Error { // Sequence has order for error results. case []string: for _, tag := range assertValue { - name, rule, msg := parseSequenceTag(tag) + name, rule, msg := ParseTagValue(tag) if len(name) == 0 { continue } @@ -97,7 +97,7 @@ func (v *Validator) doCheckMap(ctx context.Context, params interface{}) Error { validator.rules = nil validator.messages = nil for _, item := range inputParamMap { - originTypeAndKind := utils.OriginTypeAndKind(item) + originTypeAndKind := reflection.OriginTypeAndKind(item) switch originTypeAndKind.OriginKind { case reflect.Map, reflect.Struct, reflect.Slice, reflect.Array: v.doCheckValueRecursively(ctx, doCheckValueRecursivelyInput{ diff --git a/util/gvalid/gvalid_validator_check_struct.go b/util/gvalid/gvalid_validator_check_struct.go index 6a43b2b11..42244db40 100644 --- a/util/gvalid/gvalid_validator_check_struct.go +++ b/util/gvalid/gvalid_validator_check_struct.go @@ -12,8 +12,10 @@ import ( "strings" "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/internal/empty" "github.com/gogf/gf/v2/os/gstructs" "github.com/gogf/gf/v2/util/gconv" + "github.com/gogf/gf/v2/util/gmeta" "github.com/gogf/gf/v2/util/gutil" ) @@ -22,6 +24,8 @@ func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error errorMaps = make(map[string]map[string]error) // Returning error. fieldToAliasNameMap = make(map[string]string) // Field names to alias name map. resultSequenceRules = make([]fieldRule, 0) + isEmptyData = empty.IsEmpty(v.data) + isEmptyAssoc = empty.IsEmpty(v.assoc) ) fieldMap, err := gstructs.FieldMap(gstructs.FieldMapInput{ Pointer: object, @@ -38,7 +42,7 @@ func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error return newValidationErrorByStr(internalObjectErrRuleName, err) } // If there's no struct tag and validation rules, it does nothing and returns quickly. - if len(tagFields) == 0 && v.messages == nil { + if len(tagFields) == 0 && v.messages == nil && isEmptyData && isEmptyAssoc { return nil } @@ -57,7 +61,7 @@ func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error // Sequence has order for error results. case []string: for _, tag := range assertValue { - name, rule, msg := parseSequenceTag(tag) + name, rule, msg := ParseTagValue(tag) if len(name) == 0 { continue } @@ -101,17 +105,17 @@ func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error } } // If there's no struct tag and validation rules, it does nothing and returns quickly. - if len(tagFields) == 0 && len(checkRules) == 0 { + if len(tagFields) == 0 && len(checkRules) == 0 && isEmptyData && isEmptyAssoc { return nil } // Input parameter map handling. - if v.assoc == nil || !v.useDataInsteadOfObjectAttributes { + if v.assoc == nil || !v.useAssocInsteadOfObjectAttributes { inputParamMap = make(map[string]interface{}) } else { inputParamMap = gconv.Map(v.assoc) } // Checks and extends the parameters map with struct alias tag. - if !v.useDataInsteadOfObjectAttributes { + if !v.useAssocInsteadOfObjectAttributes { for nameOrTag, field := range fieldMap { inputParamMap[nameOrTag] = field.Value.Interface() if nameOrTag != field.Name() { @@ -124,8 +128,9 @@ func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error // The custom rules has the most high priority that can overwrite the struct tag rules. for _, field := range tagFields { var ( - fieldName = field.Name() // Attribute name. - name, rule, msg = parseSequenceTag(field.TagValue) // The `name` is different from `attribute alias`, which is used for validation only. + isMeta bool + fieldName = field.Name() // Attribute name. + name, rule, msg = ParseTagValue(field.TagValue) // The `name` is different from `attribute alias`, which is used for validation only. ) if len(name) == 0 { if value, ok := fieldToAliasNameMap[fieldName]; ok { @@ -142,7 +147,7 @@ func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error // It here extends the params map using alias names. // Note that the variable `name` might be alias name or attribute name. if _, ok := inputParamMap[name]; !ok { - if !v.useDataInsteadOfObjectAttributes { + if !v.useAssocInsteadOfObjectAttributes { inputParamMap[name] = field.Value.Interface() } else { if name != fieldName { @@ -168,9 +173,14 @@ func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error } } else { nameToRuleMap[name] = rule + if fieldValue := field.Value.Interface(); fieldValue != nil { + _, isMeta = fieldValue.(gmeta.Meta) + } checkRules = append(checkRules, fieldRule{ - Name: name, - Rule: rule, + Name: name, + Rule: rule, + IsMeta: isMeta, + FieldKind: field.OriginalKind(), }) } } else { @@ -215,11 +225,9 @@ func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error } // Temporary variable for value. - var ( - value interface{} - ) + var value interface{} - // It checks the struct recursively if its attribute is an embedded struct. + // It checks the struct recursively if its attribute is a struct/struct slice. for _, field := range fieldMap { // No validation interface implements check. if _, ok := field.Value.Interface().(iNoValidation); ok { @@ -263,10 +271,25 @@ func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error // The following logic is the same as some of CheckMap but with sequence support. for _, checkRuleItem := range checkRules { - _, value = gutil.MapPossibleItemByKey(inputParamMap, checkRuleItem.Name) - if value == nil { - if aliasName := fieldToAliasNameMap[checkRuleItem.Name]; aliasName != "" { - _, value = gutil.MapPossibleItemByKey(inputParamMap, aliasName) + if !checkRuleItem.IsMeta { + _, value = gutil.MapPossibleItemByKey(inputParamMap, checkRuleItem.Name) + if value == nil { + if aliasName := fieldToAliasNameMap[checkRuleItem.Name]; aliasName != "" { + _, value = gutil.MapPossibleItemByKey(inputParamMap, aliasName) + } + } + } + // Empty json string checks according to mapping field kind. + if value != nil { + switch checkRuleItem.FieldKind { + case reflect.Struct, reflect.Map: + if gconv.String(value) == emptyJsonObjectStr { + value = "" + } + case reflect.Slice, reflect.Array: + if gconv.String(value) == emptyJsonArrayStr { + value = "" + } } } // It checks each rule and its value in loop. @@ -284,7 +307,7 @@ func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error // If value is nil or empty string and has no required* rules, // it clears the error message. // ============================================================ - if value == nil || gconv.String(value) == "" { + if !checkRuleItem.IsMeta && (value == nil || gconv.String(value) == "") { required := false // rule => error for ruleKey := range errorItem { @@ -293,6 +316,11 @@ func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error required = true break } + // All custom validation rules are required rules. + if _, ok := customRuleFuncMap[ruleKey]; ok { + required = true + break + } } if !required { continue diff --git a/util/gvalid/gvalid_validator_check_value.go b/util/gvalid/gvalid_validator_check_value.go index 6d0d46978..bd69f9f77 100644 --- a/util/gvalid/gvalid_validator_check_value.go +++ b/util/gvalid/gvalid_validator_check_value.go @@ -15,10 +15,10 @@ import ( "time" "github.com/gogf/gf/v2/container/gvar" + "github.com/gogf/gf/v2/encoding/gjson" "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/internal/json" - "github.com/gogf/gf/v2/internal/utils" "github.com/gogf/gf/v2/net/gipv4" "github.com/gogf/gf/v2/net/gipv6" "github.com/gogf/gf/v2/os/gtime" @@ -538,11 +538,11 @@ func (v *Validator) doCheckSingleBuildInRules(ctx context.Context, in doCheckBui } type doCheckValueRecursivelyInput struct { - Value interface{} - Type reflect.Type - OriginKind reflect.Kind - ErrorMaps map[string]map[string]error - ResultSequenceRules *[]fieldRule + Value interface{} // Value to be validated. + Type reflect.Type // Struct/map/slice type which to be recursively validated. + OriginKind reflect.Kind // Struct/map/slice kind to be asserted in following switch case. + ErrorMaps map[string]map[string]error // The validated failed error map. + ResultSequenceRules *[]fieldRule // The validated failed rule in sequence. } func (v *Validator) doCheckValueRecursively(ctx context.Context, in doCheckValueRecursivelyInput) { @@ -564,14 +564,15 @@ func (v *Validator) doCheckValueRecursively(ctx context.Context, in doCheckValue case reflect.Map: var ( - dataMap = gconv.Map(in.Value) + dataMap = gconv.Map(in.Value) + mapTypeElem = in.Type.Elem() + mapTypeKind = mapTypeElem.Kind() ) for _, item := range dataMap { - originTypeAndKind := utils.OriginTypeAndKind(item) v.doCheckValueRecursively(ctx, doCheckValueRecursivelyInput{ Value: item, - Type: originTypeAndKind.InputType, - OriginKind: originTypeAndKind.OriginKind, + Type: mapTypeElem, + OriginKind: mapTypeKind, ErrorMaps: in.ErrorMaps, ResultSequenceRules: in.ResultSequenceRules, }) @@ -582,16 +583,20 @@ func (v *Validator) doCheckValueRecursively(ctx context.Context, in doCheckValue } case reflect.Slice, reflect.Array: - array := gconv.Interfaces(in.Value) + var array []interface{} + if gjson.Valid(in.Value) { + array = gconv.Interfaces(gconv.Bytes(in.Value)) + } else { + array = gconv.Interfaces(in.Value) + } if len(array) == 0 { return } for _, item := range array { - originTypeAndKind := utils.OriginTypeAndKind(item) v.doCheckValueRecursively(ctx, doCheckValueRecursivelyInput{ Value: item, - Type: originTypeAndKind.InputType, - OriginKind: originTypeAndKind.OriginKind, + Type: in.Type.Elem(), + OriginKind: in.Type.Elem().Kind(), ErrorMaps: in.ErrorMaps, ResultSequenceRules: in.ResultSequenceRules, }) diff --git a/util/gvalid/gvalid_z_unit_feature_checkmap_test.go b/util/gvalid/gvalid_z_unit_feature_checkmap_test.go index 9cdb0d4ba..f3fba9a7b 100755 --- a/util/gvalid/gvalid_z_unit_feature_checkmap_test.go +++ b/util/gvalid/gvalid_z_unit_feature_checkmap_test.go @@ -40,7 +40,7 @@ func Test_CheckMap2(t *testing.T) { var params interface{} gtest.C(t, func(t *gtest.T) { if err := g.Validator().Data(params).Run(context.TODO()); err == nil { - t.Assert(err, nil) + t.AssertNil(err) } }) diff --git a/util/gvalid/gvalid_z_unit_feature_checkstruct_test.go b/util/gvalid/gvalid_z_unit_feature_checkstruct_test.go index 237938d99..7e182d84c 100755 --- a/util/gvalid/gvalid_z_unit_feature_checkstruct_test.go +++ b/util/gvalid/gvalid_z_unit_feature_checkstruct_test.go @@ -35,7 +35,7 @@ func Test_CheckStruct(t *testing.T) { } obj := &Object{"john", 16} err := g.Validator().Data(obj).Rules(rules).Messages(msgs).Run(context.TODO()) - t.Assert(err, nil) + t.AssertNil(err) }) gtest.C(t, func(t *gtest.T) { @@ -133,7 +133,7 @@ func Test_CheckStruct(t *testing.T) { } var login LoginRequest err := g.Validator().Data(login).Run(context.TODO()) - t.Assert(err, nil) + t.AssertNil(err) }) gtest.C(t, func(t *gtest.T) { @@ -249,7 +249,7 @@ func Test_CheckStruct_EmbeddedObject_Attribute(t *testing.T) { obj.Name = "john" obj.Time = gtime.Now() err := g.Validator().Data(obj).Rules(rules).Messages(ruleMsg).Run(context.TODO()) - t.Assert(err, nil) + t.AssertNil(err) }) gtest.C(t, func(t *gtest.T) { type Base struct { @@ -273,7 +273,7 @@ func Test_CheckStruct_EmbeddedObject_Attribute(t *testing.T) { obj.Type = 1 obj.Name = "john" err := g.Validator().Data(obj).Rules(rules).Messages(ruleMsg).Run(context.TODO()) - t.Assert(err, nil) + t.AssertNil(err) }) } @@ -341,7 +341,7 @@ func Test_CheckStruct_Optional(t *testing.T) { Size: 10, } err := g.Validator().Data(obj).Run(context.TODO()) - t.Assert(err, nil) + t.AssertNil(err) }) gtest.C(t, func(t *gtest.T) { type Params struct { @@ -354,7 +354,7 @@ func Test_CheckStruct_Optional(t *testing.T) { Size: 10, } err := g.Validator().Data(obj).Run(context.TODO()) - t.Assert(err, nil) + t.AssertNil(err) }) gtest.C(t, func(t *gtest.T) { type Params struct { @@ -383,7 +383,7 @@ func Test_CheckStruct_NoTag(t *testing.T) { Size: 10, } err := g.Validator().Data(obj).Run(context.TODO()) - t.Assert(err, nil) + t.AssertNil(err) }) } diff --git a/util/gvalid/gvalid_z_unit_feature_custom_rule_test.go b/util/gvalid/gvalid_z_unit_feature_custom_rule_test.go index 360804602..82cf83125 100644 --- a/util/gvalid/gvalid_z_unit_feature_custom_rule_test.go +++ b/util/gvalid/gvalid_z_unit_feature_custom_rule_test.go @@ -11,8 +11,10 @@ import ( "errors" "testing" + "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/test/gtest" + "github.com/gogf/gf/v2/util/guid" "github.com/gogf/gf/v2/util/gvalid" ) @@ -37,7 +39,7 @@ func Test_CustomRule1(t *testing.T) { err := g.Validator().Data("123456").Rules(rule).Messages("custom message").Run(ctx) t.Assert(err.String(), "custom message") err = g.Validator().Data("123456").Assoc(g.Map{"data": "123456"}).Rules(rule).Messages("custom message").Run(ctx) - t.Assert(err, nil) + t.AssertNil(err) }) // Error with struct validation. gtest.C(t, func(t *gtest.T) { @@ -63,7 +65,7 @@ func Test_CustomRule1(t *testing.T) { Data: "123456", } err := g.Validator().Data(st).Run(ctx) - t.Assert(err, nil) + t.AssertNil(err) }) } @@ -106,7 +108,7 @@ func Test_CustomRule2(t *testing.T) { Data: "123456", } err := g.Validator().Data(st).Run(ctx) - t.Assert(err, nil) + t.AssertNil(err) }) } @@ -137,7 +139,7 @@ func Test_CustomRule_AllowEmpty(t *testing.T) { Data: "123456", } err := g.Validator().Data(st).Run(ctx) - t.Assert(err, nil) + t.AssertNil(err) }) // No error with struct validation. gtest.C(t, func(t *gtest.T) { @@ -271,3 +273,18 @@ func TestValidator_RuleFuncMap(t *testing.T) { t.AssertNil(err) }) } + +func Test_CustomRule_Overwrite(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var rule = "custom-" + guid.S() + gvalid.RegisterRule(rule, func(ctx context.Context, in gvalid.RuleFuncInput) error { + return gerror.New("1") + }) + t.Assert(g.Validator().Rules(rule).Data(1).Run(ctx), "1") + gvalid.RegisterRule(rule, func(ctx context.Context, in gvalid.RuleFuncInput) error { + return gerror.New("2") + }) + t.Assert(g.Validator().Rules(rule).Data(1).Run(ctx), "2") + }) + g.Dump(gvalid.GetRegisteredRuleMap()) +} diff --git a/util/gvalid/gvalid_z_unit_feature_i18n_test.go b/util/gvalid/gvalid_z_unit_feature_i18n_test.go index 2554f1837..d43d4c566 100644 --- a/util/gvalid/gvalid_z_unit_feature_i18n_test.go +++ b/util/gvalid/gvalid_z_unit_feature_i18n_test.go @@ -10,7 +10,6 @@ import ( "context" "testing" - "github.com/gogf/gf/v2/debug/gdebug" "github.com/gogf/gf/v2/i18n/gi18n" "github.com/gogf/gf/v2/test/gtest" "github.com/gogf/gf/v2/util/gvalid" @@ -19,7 +18,7 @@ import ( func TestValidator_I18n(t *testing.T) { var ( err gvalid.Error - i18nManager = gi18n.New(gi18n.Options{Path: gdebug.TestDataPath("i18n")}) + i18nManager = gi18n.New(gi18n.Options{Path: gtest.DataPath("i18n")}) ctxCn = gi18n.WithLanguage(context.TODO(), "cn") validator = gvalid.New().I18n(i18nManager) ) diff --git a/util/gvalid/gvalid_z_unit_feature_meta_test.go b/util/gvalid/gvalid_z_unit_feature_meta_test.go new file mode 100644 index 000000000..7bea9e2b2 --- /dev/null +++ b/util/gvalid/gvalid_z_unit_feature_meta_test.go @@ -0,0 +1,47 @@ +// Copyright GoFrame Author(https://goframe.org). 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 gvalid_test + +import ( + "context" + "testing" + + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/test/gtest" + "github.com/gogf/gf/v2/util/gvalid" +) + +type UserCreateReq struct { + g.Meta `v:"UserCreateReq"` + Name string + Pass string +} + +func RuleUserCreateReq(ctx context.Context, in gvalid.RuleFuncInput) error { + var req *UserCreateReq + if err := in.Data.Scan(&req); err != nil { + return gerror.Wrap(err, `Scan data to UserCreateReq failed`) + } + return gerror.Newf(`The name "%s" is already token by others`, req.Name) +} + +func Test_Meta(t *testing.T) { + var user = &UserCreateReq{ + Name: "john", + Pass: "123456", + } + + gtest.C(t, func(t *gtest.T) { + err := g.Validator().RuleFunc("UserCreateReq", RuleUserCreateReq). + Data(user). + Assoc(g.Map{ + "Name": "john smith", + }).Run(ctx) + t.Assert(err.String(), `The name "john smith" is already token by others`) + }) +} diff --git a/util/gvalid/gvalid_z_unit_feature_recursive_test.go b/util/gvalid/gvalid_z_unit_feature_recursive_test.go index b441f7c70..60401056c 100755 --- a/util/gvalid/gvalid_z_unit_feature_recursive_test.go +++ b/util/gvalid/gvalid_z_unit_feature_recursive_test.go @@ -214,3 +214,110 @@ func Test_CheckMap_Recursive_SliceStruct(t *testing.T) { t.Assert(err.Maps()["Pass2"], g.Map{"same": "The Pass2 value `4` must be the same as field Pass1"}) }) } + +func Test_CheckStruct_Recursively_SliceAttribute(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + type Student struct { + Name string `v:"required#Student Name is required"` + Age int `v:"required"` + } + type Teacher struct { + Name string `v:"required#Teacher Name is required"` + Students []Student `v:"required"` + } + var ( + teacher = Teacher{} + data = g.Map{ + "name": "john", + "students": `[]`, + } + ) + err := g.Validator().Assoc(data).Data(teacher).Run(ctx) + t.Assert(err, `The Students field is required`) + }) + + gtest.C(t, func(t *gtest.T) { + type Student struct { + Name string `v:"required#Student Name is required"` + Age int `v:"required"` + } + type Teacher struct { + Name string `v:"required#Teacher Name is required"` + Students []Student + } + var ( + teacher = Teacher{} + data = g.Map{ + "name": "john", + } + ) + err := g.Validator().Assoc(data).Data(teacher).Run(ctx) + t.Assert(err, ``) + }) + + gtest.C(t, func(t *gtest.T) { + type Student struct { + Name string `v:"required#Student Name is required"` + Age int `v:"required"` + } + type Teacher struct { + Name string `v:"required#Teacher Name is required"` + Students []Student `v:"required"` + } + var ( + teacher = Teacher{} + data = g.Map{ + "name": "john", + "students": `[{"age":2}, {"name":"jack", "age":4}]`, + } + ) + err := g.Validator().Assoc(data).Data(teacher).Run(ctx) + t.Assert(err, `Student Name is required`) + }) +} + +func Test_CheckStruct_Recursively_SliceAttribute_WithTypeAlias(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + type ParamsItemBase struct { + Component string `v:"required" dc:"组件名称"` + Params string `v:"required" dc:"配置参数(一般是JSON)"` + Version uint64 `v:"required" dc:"参数版本"` + } + type ParamsItem = ParamsItemBase + type ParamsModifyReq struct { + Revision uint64 `v:"required"` + BizParams []ParamsItem `v:"required"` + } + var ( + req = ParamsModifyReq{} + data = g.Map{ + "Revision": "1", + "BizParams": `[{}]`, + } + ) + err := g.Validator().Assoc(data).Data(req).Run(ctx) + t.Assert(err, `The Component field is required; The Params field is required; The Version field is required`) + }) +} + +func Test_CheckStruct_Recursively_MapAttribute(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + type Student struct { + Name string `v:"required#Student Name is required"` + Age int `v:"required"` + } + type Teacher struct { + Name string `v:"required#Teacher Name is required"` + Students map[string]Student `v:"required"` + } + var ( + teacher = Teacher{} + data = g.Map{ + "name": "john", + "students": `{"john":{"age":18}}`, + } + ) + err := g.Validator().Assoc(data).Data(teacher).Run(ctx) + t.Assert(err, `Student Name is required`) + }) +} diff --git a/util/gvalid/gvalid_z_unit_feature_rule_test.go b/util/gvalid/gvalid_z_unit_feature_rule_test.go index f249ed73b..5229ef9b4 100755 --- a/util/gvalid/gvalid_z_unit_feature_rule_test.go +++ b/util/gvalid/gvalid_z_unit_feature_rule_test.go @@ -670,7 +670,7 @@ func Test_Domain(t *testing.T) { err = g.Validator().Data(k).Rules("domain").Run(ctx) if v { // fmt.Println(k) - t.Assert(err, nil) + t.AssertNil(err) } else { // fmt.Println(k) t.AssertNE(err, nil) diff --git a/util/gvalid/gvalid_z_unit_internal_test.go b/util/gvalid/gvalid_z_unit_internal_test.go index c1edc90ca..4a86d5053 100644 --- a/util/gvalid/gvalid_z_unit_internal_test.go +++ b/util/gvalid/gvalid_z_unit_internal_test.go @@ -15,30 +15,36 @@ import ( func Test_parseSequenceTag(t *testing.T) { gtest.C(t, func(t *gtest.T) { s := "name@required|length:2,20|password3|same:password1#||密码强度不足|两次密码不一致" - field, rule, msg := parseSequenceTag(s) + field, rule, msg := ParseTagValue(s) t.Assert(field, "name") t.Assert(rule, "required|length:2,20|password3|same:password1") t.Assert(msg, "||密码强度不足|两次密码不一致") }) gtest.C(t, func(t *gtest.T) { s := "required|length:2,20|password3|same:password1#||密码强度不足|两次密码不一致" - field, rule, msg := parseSequenceTag(s) + field, rule, msg := ParseTagValue(s) t.Assert(field, "") t.Assert(rule, "required|length:2,20|password3|same:password1") t.Assert(msg, "||密码强度不足|两次密码不一致") }) gtest.C(t, func(t *gtest.T) { s := "required|length:2,20|password3|same:password1" - field, rule, msg := parseSequenceTag(s) + field, rule, msg := ParseTagValue(s) t.Assert(field, "") t.Assert(rule, "required|length:2,20|password3|same:password1") t.Assert(msg, "") }) gtest.C(t, func(t *gtest.T) { s := "required" - field, rule, msg := parseSequenceTag(s) + field, rule, msg := ParseTagValue(s) t.Assert(field, "") t.Assert(rule, "required") t.Assert(msg, "") }) } + +func Test_GetTags(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + t.Assert(structTagPriority, GetTags()) + }) +} diff --git a/version.go b/version.go index de38fb4b1..2369cb189 100644 --- a/version.go +++ b/version.go @@ -1,4 +1,4 @@ package gf -const VERSION = "v2.0.0-beta" +const VERSION = "v2.0.6" const AUTHORS = "john"