From 3841f05e024d2a02ab01d373dde580ab5534c976 Mon Sep 17 00:00:00 2001 From: John Guo Date: Mon, 28 Aug 2023 21:49:30 +0800 Subject: [PATCH] add `AdapterContent` implements for `gcfg.Adapter` (#2892) --- os/gcfg/gcfg_adapter_content.go | 79 +++++++++++++++++++++ os/gcfg/gcfg_adapter_file.go | 69 +++++++++--------- os/gcfg/gcfg_adapter_file_content.go | 8 +-- os/gcfg/gcfg_adapter_file_path.go | 56 +++++++-------- os/gcfg/gcfg_z_unit_adapter_content_test.go | 70 ++++++++++++++++++ os/gtimer/gtimer.go | 2 +- 6 files changed, 217 insertions(+), 67 deletions(-) create mode 100644 os/gcfg/gcfg_adapter_content.go create mode 100644 os/gcfg/gcfg_z_unit_adapter_content_test.go diff --git a/os/gcfg/gcfg_adapter_content.go b/os/gcfg/gcfg_adapter_content.go new file mode 100644 index 000000000..44daa7ef1 --- /dev/null +++ b/os/gcfg/gcfg_adapter_content.go @@ -0,0 +1,79 @@ +// 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 gcfg + +import ( + "context" + + "github.com/gogf/gf/v2/container/gvar" + "github.com/gogf/gf/v2/encoding/gjson" + "github.com/gogf/gf/v2/errors/gerror" +) + +// AdapterContent implements interface Adapter using content. +// The configuration content supports the coding types as package `gjson`. +type AdapterContent struct { + jsonVar *gvar.Var // The pared JSON object for configuration content, type: *gjson.Json. +} + +// NewAdapterContent returns a new configuration management object using custom content. +// The parameter `content` specifies the default configuration content for reading. +func NewAdapterContent(content ...string) (*AdapterContent, error) { + a := &AdapterContent{ + jsonVar: gvar.New(nil, true), + } + if len(content) > 0 { + if err := a.SetContent(content[0]); err != nil { + return nil, err + } + } + return a, nil +} + +// SetContent sets customized configuration content for specified `file`. +// The `file` is unnecessary param, default is DefaultConfigFile. +func (a *AdapterContent) SetContent(content string) error { + j, err := gjson.LoadContent(content, true) + if err != nil { + return gerror.Wrap(err, `load configuration content failed`) + } + a.jsonVar.Set(j) + return nil +} + +// Available checks and returns the backend configuration service is available. +// The optional parameter `resource` specifies certain configuration resource. +// +// Note that this function does not return error as it just does simply check for +// backend configuration service. +func (a *AdapterContent) Available(ctx context.Context, resource ...string) (ok bool) { + if a.jsonVar.IsNil() { + return false + } + return true +} + +// Get retrieves and returns value by specified `pattern` in current resource. +// Pattern like: +// "x.y.z" for map item. +// "x.0.y" for slice item. +func (a *AdapterContent) Get(ctx context.Context, pattern string) (value interface{}, err error) { + if a.jsonVar.IsNil() { + return nil, nil + } + return a.jsonVar.Val().(*gjson.Json).Get(pattern).Val(), nil +} + +// Data retrieves and returns all configuration data in current resource as map. +// Note that this function may lead lots of memory usage if configuration data is too large, +// you can implement this function if necessary. +func (a *AdapterContent) Data(ctx context.Context) (data map[string]interface{}, err error) { + if a.jsonVar.IsNil() { + return nil, nil + } + return a.jsonVar.Val().(*gjson.Json).Var().Map(), nil +} diff --git a/os/gcfg/gcfg_adapter_file.go b/os/gcfg/gcfg_adapter_file.go index a33d3775e..d10e21fb4 100644 --- a/os/gcfg/gcfg_adapter_file.go +++ b/os/gcfg/gcfg_adapter_file.go @@ -23,6 +23,7 @@ import ( "github.com/gogf/gf/v2/util/gutil" ) +// AdapterFile implements interface Adapter using file. type AdapterFile struct { defaultName string // Default configuration file name. searchPaths *garray.StrArray // Searching path array. @@ -113,19 +114,19 @@ func NewAdapterFile(file ...string) (*AdapterFile, error) { // // Note that, turning on this feature is quite expensive, and it is not recommended // allowing separators in the key names. It is best to avoid this on the application side. -func (c *AdapterFile) SetViolenceCheck(check bool) { - c.violenceCheck = check - c.Clear() +func (a *AdapterFile) SetViolenceCheck(check bool) { + a.violenceCheck = check + a.Clear() } // SetFileName sets the default configuration file name. -func (c *AdapterFile) SetFileName(name string) { - c.defaultName = name +func (a *AdapterFile) SetFileName(name string) { + a.defaultName = name } // GetFileName returns the default configuration file name. -func (c *AdapterFile) GetFileName() string { - return c.defaultName +func (a *AdapterFile) GetFileName() string { + return a.defaultName } // Get retrieves and returns value by specified `pattern`. @@ -136,8 +137,8 @@ func (c *AdapterFile) GetFileName() string { // "list.10", "array.0.name", "array.0.1.id". // // It returns a default value specified by `def` if value for `pattern` is not found. -func (c *AdapterFile) Get(ctx context.Context, pattern string) (value interface{}, err error) { - j, err := c.getJson() +func (a *AdapterFile) Get(ctx context.Context, pattern string) (value interface{}, err error) { + j, err := a.getJson() if err != nil { return nil, err } @@ -152,8 +153,8 @@ func (c *AdapterFile) Get(ctx context.Context, pattern string) (value interface{ // It is commonly used for updates certain configuration value in runtime. // Note that, it is not recommended using `Set` configuration at runtime as the configuration would be // automatically refreshed if underlying configuration file changed. -func (c *AdapterFile) Set(pattern string, value interface{}) error { - j, err := c.getJson() +func (a *AdapterFile) Set(pattern string, value interface{}) error { + j, err := a.getJson() if err != nil { return err } @@ -164,8 +165,8 @@ func (c *AdapterFile) Set(pattern string, value interface{}) error { } // Data retrieves and returns all configuration data as map type. -func (c *AdapterFile) Data(ctx context.Context) (data map[string]interface{}, err error) { - j, err := c.getJson() +func (a *AdapterFile) Data(ctx context.Context) (data map[string]interface{}, err error) { + j, err := a.getJson() if err != nil { return nil, err } @@ -176,8 +177,8 @@ func (c *AdapterFile) Data(ctx context.Context) (data map[string]interface{}, er } // MustGet acts as function Get, but it panics if error occurs. -func (c *AdapterFile) MustGet(ctx context.Context, pattern string) *gvar.Var { - v, err := c.Get(ctx, pattern) +func (a *AdapterFile) MustGet(ctx context.Context, pattern string) *gvar.Var { + v, err := a.Get(ctx, pattern) if err != nil { panic(err) } @@ -186,26 +187,26 @@ func (c *AdapterFile) MustGet(ctx context.Context, pattern string) *gvar.Var { // Clear removes all parsed configuration files content cache, // which will force reload configuration content from file. -func (c *AdapterFile) Clear() { - c.jsonMap.Clear() +func (a *AdapterFile) Clear() { + a.jsonMap.Clear() } // Dump prints current Json object with more manually readable. -func (c *AdapterFile) Dump() { - if j, _ := c.getJson(); j != nil { +func (a *AdapterFile) Dump() { + if j, _ := a.getJson(); j != nil { j.Dump() } } // Available checks and returns whether configuration of given `file` is available. -func (c *AdapterFile) Available(ctx context.Context, fileName ...string) bool { - checkFileName := gutil.GetOrDefaultStr(c.defaultName, fileName...) +func (a *AdapterFile) Available(ctx context.Context, fileName ...string) bool { + checkFileName := gutil.GetOrDefaultStr(a.defaultName, fileName...) // Custom configuration content exists. - if c.GetContent(checkFileName) != "" { + if a.GetContent(checkFileName) != "" { return true } // Configuration file exists in system path. - if path, _ := c.GetFilePath(checkFileName); path != "" { + if path, _ := a.GetFilePath(checkFileName); path != "" { return true } return false @@ -213,12 +214,12 @@ func (c *AdapterFile) Available(ctx context.Context, fileName ...string) bool { // autoCheckAndAddMainPkgPathToSearchPaths automatically checks and adds directory path of package main // to the searching path list if it's currently in development environment. -func (c *AdapterFile) autoCheckAndAddMainPkgPathToSearchPaths() { +func (a *AdapterFile) autoCheckAndAddMainPkgPathToSearchPaths() { if gmode.IsDevelop() { mainPkgPath := gfile.MainPkgPath() if mainPkgPath != "" { - if !c.searchPaths.Contains(mainPkgPath) { - c.searchPaths.Append(mainPkgPath) + if !a.searchPaths.Contains(mainPkgPath) { + a.searchPaths.Append(mainPkgPath) } } } @@ -226,26 +227,26 @@ func (c *AdapterFile) autoCheckAndAddMainPkgPathToSearchPaths() { // getJson returns a *gjson.Json object for the specified `file` content. // It would print error if file reading fails. It returns nil if any error occurs. -func (c *AdapterFile) getJson(fileName ...string) (configJson *gjson.Json, err error) { +func (a *AdapterFile) getJson(fileName ...string) (configJson *gjson.Json, err error) { var ( - usedFileName = c.defaultName + usedFileName = a.defaultName ) if len(fileName) > 0 && fileName[0] != "" { usedFileName = fileName[0] } else { - usedFileName = c.defaultName + usedFileName = a.defaultName } // It uses json map to cache specified configuration file content. - result := c.jsonMap.GetOrSetFuncLock(usedFileName, func() interface{} { + result := a.jsonMap.GetOrSetFuncLock(usedFileName, func() interface{} { var ( content string filePath string ) // The configured content can be any kind of data type different from its file type. isFromConfigContent := true - if content = c.GetContent(usedFileName); content == "" { + if content = a.GetContent(usedFileName); content == "" { isFromConfigContent = false - filePath, err = c.GetFilePath(usedFileName) + filePath, err = a.GetFilePath(usedFileName) if err != nil { return nil } @@ -273,12 +274,12 @@ func (c *AdapterFile) getJson(fileName ...string) (configJson *gjson.Json, err e } return nil } - configJson.SetViolenceCheck(c.violenceCheck) + configJson.SetViolenceCheck(a.violenceCheck) // Add monitor for this configuration file, // any changes of this file will refresh its cache in Config object. if filePath != "" && !gres.Contains(filePath) { _, err = gfsnotify.Add(filePath, func(event *gfsnotify.Event) { - c.jsonMap.Remove(usedFileName) + a.jsonMap.Remove(usedFileName) }) if err != nil { return nil diff --git a/os/gcfg/gcfg_adapter_file_content.go b/os/gcfg/gcfg_adapter_file_content.go index b709fe393..4a99c02ca 100644 --- a/os/gcfg/gcfg_adapter_file_content.go +++ b/os/gcfg/gcfg_adapter_file_content.go @@ -14,7 +14,7 @@ import ( // SetContent sets customized configuration content for specified `file`. // The `file` is unnecessary param, default is DefaultConfigFile. -func (c *AdapterFile) SetContent(content string, file ...string) { +func (a *AdapterFile) SetContent(content string, file ...string) { name := DefaultConfigFileName if len(file) > 0 { name = file[0] @@ -36,7 +36,7 @@ func (c *AdapterFile) SetContent(content string, file ...string) { // GetContent returns customized configuration content for specified `file`. // The `file` is unnecessary param, default is DefaultConfigFile. -func (c *AdapterFile) GetContent(file ...string) string { +func (a *AdapterFile) GetContent(file ...string) string { name := DefaultConfigFileName if len(file) > 0 { name = file[0] @@ -46,7 +46,7 @@ func (c *AdapterFile) GetContent(file ...string) string { // RemoveContent removes the global configuration with specified `file`. // If `name` is not passed, it removes configuration of the default group name. -func (c *AdapterFile) RemoveContent(file ...string) { +func (a *AdapterFile) RemoveContent(file ...string) { name := DefaultConfigFileName if len(file) > 0 { name = file[0] @@ -69,7 +69,7 @@ func (c *AdapterFile) RemoveContent(file ...string) { } // ClearContent removes all global configuration contents. -func (c *AdapterFile) ClearContent() { +func (a *AdapterFile) ClearContent() { customConfigContentMap.Clear() // Clear cache for all instances. localInstances.LockFunc(func(m map[string]interface{}) { diff --git a/os/gcfg/gcfg_adapter_file_path.go b/os/gcfg/gcfg_adapter_file_path.go index 1db5c0189..53cb2765e 100644 --- a/os/gcfg/gcfg_adapter_file_path.go +++ b/os/gcfg/gcfg_adapter_file_path.go @@ -24,7 +24,7 @@ import ( // SetPath sets the configuration directory path for file search. // The parameter `path` can be absolute or relative path, // but absolute path is strongly recommended. -func (c *AdapterFile) SetPath(path string) (err error) { +func (a *AdapterFile) SetPath(path string) (err error) { var ( isDir = false realPath = "" @@ -37,7 +37,7 @@ func (c *AdapterFile) SetPath(path string) (err error) { realPath = gfile.RealPath(path) if realPath == "" { // Relative path. - c.searchPaths.RLockFunc(func(array []string) { + a.searchPaths.RLockFunc(func(array []string) { for _, v := range array { if searchedPath, _ := gspath.Search(v, path); searchedPath != "" { realPath = searchedPath @@ -53,9 +53,9 @@ func (c *AdapterFile) SetPath(path string) (err error) { // Path not exist. if realPath == "" { buffer := bytes.NewBuffer(nil) - if c.searchPaths.Len() > 0 { + if a.searchPaths.Len() > 0 { buffer.WriteString(fmt.Sprintf(`SetPath failed: cannot find directory "%s" in following paths:`, path)) - c.searchPaths.RLockFunc(func(array []string) { + a.searchPaths.RLockFunc(func(array []string) { for k, v := range array { buffer.WriteString(fmt.Sprintf("\n%d. %s", k+1, v)) } @@ -74,20 +74,20 @@ func (c *AdapterFile) SetPath(path string) (err error) { ) } // Repeated path check. - if c.searchPaths.Search(realPath) != -1 { + if a.searchPaths.Search(realPath) != -1 { return nil } - c.jsonMap.Clear() - c.searchPaths.Clear() - c.searchPaths.Append(realPath) + a.jsonMap.Clear() + a.searchPaths.Clear() + a.searchPaths.Append(realPath) intlog.Print(context.TODO(), "SetPath:", realPath) return nil } // AddPath adds an absolute or relative path to the search paths. -func (c *AdapterFile) AddPath(paths ...string) (err error) { +func (a *AdapterFile) AddPath(paths ...string) (err error) { for _, path := range paths { - if err = c.doAddPath(path); err != nil { + if err = a.doAddPath(path); err != nil { return err } } @@ -95,7 +95,7 @@ func (c *AdapterFile) AddPath(paths ...string) (err error) { } // doAddPath adds an absolute or relative path to the search paths. -func (c *AdapterFile) doAddPath(path string) (err error) { +func (a *AdapterFile) doAddPath(path string) (err error) { var ( isDir = false realPath = "" @@ -110,7 +110,7 @@ func (c *AdapterFile) doAddPath(path string) (err error) { realPath = gfile.RealPath(path) if realPath == "" { // Relative path. - c.searchPaths.RLockFunc(func(array []string) { + a.searchPaths.RLockFunc(func(array []string) { for _, v := range array { if searchedPath, _ := gspath.Search(v, path); searchedPath != "" { realPath = searchedPath @@ -125,9 +125,9 @@ func (c *AdapterFile) doAddPath(path string) (err error) { } if realPath == "" { buffer := bytes.NewBuffer(nil) - if c.searchPaths.Len() > 0 { + if a.searchPaths.Len() > 0 { buffer.WriteString(fmt.Sprintf(`AddPath failed: cannot find directory "%s" in following paths:`, path)) - c.searchPaths.RLockFunc(func(array []string) { + a.searchPaths.RLockFunc(func(array []string) { for k, v := range array { buffer.WriteString(fmt.Sprintf("\n%d. %s", k+1, v)) } @@ -141,23 +141,23 @@ func (c *AdapterFile) doAddPath(path string) (err error) { return gerror.NewCodef(gcode.CodeInvalidParameter, `AddPath failed: path "%s" should be directory type`, path) } // Repeated path check. - if c.searchPaths.Search(realPath) != -1 { + if a.searchPaths.Search(realPath) != -1 { return nil } - c.searchPaths.Append(realPath) + a.searchPaths.Append(realPath) intlog.Print(context.TODO(), "AddPath:", realPath) return nil } // GetPaths returns the searching path array of current configuration manager. -func (c *AdapterFile) GetPaths() []string { - return c.searchPaths.Slice() +func (a *AdapterFile) GetPaths() []string { + return a.searchPaths.Slice() } // doGetFilePath returns the absolute configuration file path for the given filename by `file`. // If `file` is not passed, it returns the configuration file path of the default name. // It returns an empty `path` string and an error if the given `file` does not exist. -func (c *AdapterFile) doGetFilePath(fileName string) (path string) { +func (a *AdapterFile) doGetFilePath(fileName string) (path string) { var ( tempPath string resFile *gres.File @@ -175,7 +175,7 @@ func (c *AdapterFile) doGetFilePath(fileName string) (path string) { } } } - c.searchPaths.RLockFunc(func(array []string) { + a.searchPaths.RLockFunc(func(array []string) { for _, searchPath := range array { for _, tryFolder := range resourceTryFolders { tempPath = searchPath + tryFolder + fileName @@ -191,7 +191,7 @@ func (c *AdapterFile) doGetFilePath(fileName string) (path string) { }) } - c.autoCheckAndAddMainPkgPathToSearchPaths() + a.autoCheckAndAddMainPkgPathToSearchPaths() // Searching local file system. if path == "" { @@ -199,7 +199,7 @@ func (c *AdapterFile) doGetFilePath(fileName string) (path string) { if path = gfile.RealPath(fileName); path != "" && !gfile.IsDir(path) { return } - c.searchPaths.RLockFunc(func(array []string) { + a.searchPaths.RLockFunc(func(array []string) { for _, searchPath := range array { searchPath = gstr.TrimRight(searchPath, `\/`) for _, tryFolder := range localSystemTryFolders { @@ -220,23 +220,23 @@ func (c *AdapterFile) doGetFilePath(fileName string) (path string) { // GetFilePath returns the absolute configuration file path for the given filename by `file`. // If `file` is not passed, it returns the configuration file path of the default name. // It returns an empty `path` string and an error if the given `file` does not exist. -func (c *AdapterFile) GetFilePath(fileName ...string) (path string, err error) { +func (a *AdapterFile) GetFilePath(fileName ...string) (path string, err error) { var ( fileExtName string tempFileName string - usedFileName = c.defaultName + usedFileName = a.defaultName ) if len(fileName) > 0 { usedFileName = fileName[0] } fileExtName = gfile.ExtName(usedFileName) - if path = c.doGetFilePath(usedFileName); (path == "" || gfile.IsDir(path)) && !gstr.InArray(supportedFileTypes, fileExtName) { + if path = a.doGetFilePath(usedFileName); (path == "" || gfile.IsDir(path)) && !gstr.InArray(supportedFileTypes, fileExtName) { // If it's not using default configuration or its configuration file is not available, // it searches the possible configuration file according to the name and all supported // file types. for _, fileType := range supportedFileTypes { tempFileName = fmt.Sprintf(`%s.%s`, usedFileName, fileType) - if path = c.doGetFilePath(tempFileName); path != "" { + if path = a.doGetFilePath(tempFileName); path != "" { break } } @@ -244,7 +244,7 @@ func (c *AdapterFile) GetFilePath(fileName ...string) (path string, err error) { // If it cannot find the path of `file`, it formats and returns a detailed error. if path == "" { var buffer = bytes.NewBuffer(nil) - if c.searchPaths.Len() > 0 { + if a.searchPaths.Len() > 0 { if !gstr.InArray(supportedFileTypes, fileExtName) { buffer.WriteString(fmt.Sprintf( `possible config files "%s" or "%s" not found in resource manager or following system searching paths:`, @@ -256,7 +256,7 @@ func (c *AdapterFile) GetFilePath(fileName ...string) (path string, err error) { usedFileName, )) } - c.searchPaths.RLockFunc(func(array []string) { + a.searchPaths.RLockFunc(func(array []string) { index := 1 for _, searchPath := range array { searchPath = gstr.TrimRight(searchPath, `\/`) diff --git a/os/gcfg/gcfg_z_unit_adapter_content_test.go b/os/gcfg/gcfg_z_unit_adapter_content_test.go new file mode 100644 index 000000000..8a30f1316 --- /dev/null +++ b/os/gcfg/gcfg_z_unit_adapter_content_test.go @@ -0,0 +1,70 @@ +// 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. + +// go test *.go -bench=".*" -benchmem + +package gcfg_test + +import ( + "testing" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gcfg" + "github.com/gogf/gf/v2/test/gtest" +) + +func TestAdapterContent_Available_Get_Data(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + adapter, err := gcfg.NewAdapterContent() + t.AssertNil(err) + t.Assert(adapter.Available(ctx), false) + }) + gtest.C(t, func(t *gtest.T) { + content := `{"a": 1, "b": 2, "c": {"d": 3}}` + adapter, err := gcfg.NewAdapterContent(content) + t.AssertNil(err) + + c := gcfg.NewWithAdapter(adapter) + t.Assert(c.Available(ctx), true) + t.Assert(c.MustGet(ctx, "a"), 1) + t.Assert(c.MustGet(ctx, "b"), 2) + t.Assert(c.MustGet(ctx, "c.d"), 3) + t.Assert(c.MustGet(ctx, "d"), nil) + t.Assert(c.MustData(ctx), g.Map{ + "a": 1, + "b": 2, + "c": g.Map{ + "d": 3, + }, + }) + }) +} + +func TestAdapterContent_SetContent(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + adapter, err := gcfg.NewAdapterContent() + t.AssertNil(err) + t.Assert(adapter.Available(ctx), false) + + content := `{"a": 1, "b": 2, "c": {"d": 3}}` + err = adapter.SetContent(content) + t.AssertNil(err) + c := gcfg.NewWithAdapter(adapter) + t.Assert(c.Available(ctx), true) + t.Assert(c.MustGet(ctx, "a"), 1) + t.Assert(c.MustGet(ctx, "b"), 2) + t.Assert(c.MustGet(ctx, "c.d"), 3) + t.Assert(c.MustGet(ctx, "d"), nil) + t.Assert(c.MustData(ctx), g.Map{ + "a": 1, + "b": 2, + "c": g.Map{ + "d": 3, + }, + }) + }) + +} diff --git a/os/gtimer/gtimer.go b/os/gtimer/gtimer.go index 7988d4d25..9c335ecb0 100644 --- a/os/gtimer/gtimer.go +++ b/os/gtimer/gtimer.go @@ -41,7 +41,7 @@ type Timer struct { // TimerOptions is the configuration object for Timer. type TimerOptions struct { - Interval time.Duration // Interval is the interval escaped of the timer. + Interval time.Duration // (optional) Interval is the underlying rolling interval tick of the timer. Quick bool // Quick is used for quick timer, which means the timer will not wait for the first interval to be elapsed. }