diff --git a/internal/command/command.go b/internal/command/command.go index 734743b87..e704e6a9f 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -33,30 +33,38 @@ func Init(args ...string) { defaultParsedOptions = make(map[string]string) } // Parsing os.Args with default algorithm. + defaultParsedArgs, defaultParsedOptions = ParseUsingDefaultAlgorithm(args...) +} + +// ParseUsingDefaultAlgorithm parses arguments using default algorithm. +func ParseUsingDefaultAlgorithm(args ...string) (parsedArgs []string, parsedOptions map[string]string) { + parsedArgs = make([]string, 0) + parsedOptions = make(map[string]string) for i := 0; i < len(args); { array := argumentRegex.FindStringSubmatch(args[i]) if len(array) > 2 { if array[2] == "=" { - defaultParsedOptions[array[1]] = array[3] + parsedOptions[array[1]] = array[3] } else if i < len(args)-1 { if len(args[i+1]) > 0 && args[i+1][0] == '-' { // Eg: gf gen -d -n 1 - defaultParsedOptions[array[1]] = array[3] + parsedOptions[array[1]] = array[3] } else { // Eg: gf gen -n 2 - defaultParsedOptions[array[1]] = args[i+1] + parsedOptions[array[1]] = args[i+1] i += 2 continue } } else { // Eg: gf gen -h - defaultParsedOptions[array[1]] = array[3] + parsedOptions[array[1]] = array[3] } } else { - defaultParsedArgs = append(defaultParsedArgs, args[i]) + parsedArgs = append(parsedArgs, args[i]) } i++ } + return } // GetOpt returns the option value named `name`. diff --git a/os/gcache/gcache_z_unit_test.go b/os/gcache/gcache_z_unit_test.go index 648b2e943..1f10b4223 100644 --- a/os/gcache/gcache_z_unit_test.go +++ b/os/gcache/gcache_z_unit_test.go @@ -119,7 +119,7 @@ func TestCache_UpdateExpire(t *testing.T) { newExpire := 10 * time.Second oldExpire2, err := gcache.UpdateExpire(ctx, key, newExpire) t.AssertNil(err) - t.Assert(oldExpire2, oldExpire) + t.AssertIN(oldExpire2, g.Slice{oldExpire, `2.999s`}) e, _ := gcache.GetExpire(ctx, key) t.AssertNE(e, oldExpire) diff --git a/os/gcmd/gcmd.go b/os/gcmd/gcmd.go index 5c80d0f5f..8278521a0 100644 --- a/os/gcmd/gcmd.go +++ b/os/gcmd/gcmd.go @@ -16,8 +16,10 @@ import ( "github.com/gogf/gf/v2/internal/utils" ) -var ( - defaultCommandFuncMap = make(map[string]func()) +const ( + helpOptionName = "help" + helpOptionNameShort = "h" + maxLineChars = 120 ) // Init does custom initialization. diff --git a/os/gcmd/gcmd_command.go b/os/gcmd/gcmd_command.go new file mode 100644 index 000000000..2be2c7237 --- /dev/null +++ b/os/gcmd/gcmd_command.go @@ -0,0 +1,68 @@ +// 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 gcmd + +import ( + "context" + + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/text/gstr" +) + +// Command holds the info about an argument that can handle custom logic. +type Command struct { + Name string // Command name(case-sensitive). + Usage string // A brief line description about its usage, eg: gf build main.go [OPTION] + Brief string // A brief info that describes what this command will do. + Description string // A detailed description. + Options []Option // Option array, configuring how this command act. + Func Function // Custom function. + HelpFunc Function // Custom help function + Examples string // Usage examples. + Additional string // Additional custom info about this command. + parent *Command // Parent command for internal usage. + commands []Command // Sub commands of this command. +} + +// Function is a custom command callback function that is bound to a certain argument. +type Function func(ctx context.Context, parser *Parser) (err error) + +// Option is the command value that is specified by a name or shor name. +// An Option can have or have no value bound to it. +type Option struct { + Name string // Option name. + Short string // Option short. + Brief string // Brief info about this Option, which is used in help info. + NeedValue bool // Whether this Option having or having no value bound to it. +} + +var ( + // defaultHelpOption is the default help option that will be automatically added to each command. + defaultHelpOption = Option{ + Name: `help`, + Short: `h`, + Brief: `more information about this command`, + NeedValue: false, + } +) + +// Add adds one or more sub-commands to current command. +func (c *Command) Add(commands ...Command) error { + for _, cmd := range commands { + cmd.Name = gstr.Trim(cmd.Name) + if cmd.Name == "" { + return gerror.New("command name should not be empty") + } + if cmd.Func == nil { + return gerror.New("command function should not be empty") + } + cmd.parent = c + c.commands = append(c.commands, cmd) + } + return nil +} diff --git a/os/gcmd/gcmd_command_help.go b/os/gcmd/gcmd_command_help.go new file mode 100644 index 000000000..1e482650e --- /dev/null +++ b/os/gcmd/gcmd_command_help.go @@ -0,0 +1,131 @@ +// 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 gcmd + +import ( + "bytes" + "context" + "fmt" + + "github.com/gogf/gf/v2/text/gstr" +) + +// Print prints help info to stdout for current command. +func (c *Command) Print() { + prefix := gstr.Repeat(" ", 4) + buffer := bytes.NewBuffer(nil) + // Usage. + if c.Usage != "" || c.Name != "" { + buffer.WriteString("USAGE\n") + buffer.WriteString(prefix) + if c.Usage != "" { + buffer.WriteString(c.Usage) + } else { + var ( + p = c + name = c.Name + ) + for p.parent != nil { + name = p.parent.Name + " " + name + p = p.parent + } + buffer.WriteString(fmt.Sprintf(`%s ARGUMENT [OPTION]`, name)) + } + buffer.WriteString("\n\n") + } + // Command. + if len(c.commands) > 0 { + buffer.WriteString("COMMAND\n") + var ( + maxSpaceLength = 0 + ) + for _, cmd := range c.commands { + if len(cmd.Name) > maxSpaceLength { + maxSpaceLength = len(cmd.Name) + } + } + for _, cmd := range c.commands { + // Add "..." to brief for those commands that also have sub-commands. + if len(cmd.commands) > 0 { + cmd.Brief = gstr.TrimRight(cmd.Brief, ".") + "..." + } + var ( + spaceLength = maxSpaceLength - len(cmd.Name) + lineStr = fmt.Sprintf("%s%s%s%s\n", prefix, cmd.Name, gstr.Repeat(" ", spaceLength+4), cmd.Brief) + wordwrapPrefix = gstr.Repeat(" ", len(prefix+cmd.Name)+spaceLength+4) + ) + lineStr = gstr.WordWrap(lineStr, maxLineChars, "\n"+wordwrapPrefix) + buffer.WriteString(lineStr) + } + buffer.WriteString("\n") + } + + // Option. + if len(c.Options) > 0 { + buffer.WriteString("OPTION\n") + var ( + nameStr string + maxSpaceLength = 0 + ) + for _, option := range c.Options { + if option.Short != "" { + nameStr = fmt.Sprintf("-%s,\t--%s", option.Short, option.Name) + } else { + nameStr = fmt.Sprintf("-/--%s", option.Name) + } + if len(nameStr) > maxSpaceLength { + maxSpaceLength = len(nameStr) + } + } + for _, option := range c.Options { + if option.Short != "" { + nameStr = fmt.Sprintf("-%s,\t--%s", option.Short, option.Name) + } else { + nameStr = fmt.Sprintf("-/--%s", option.Name) + } + var ( + spaceLength = maxSpaceLength - len(nameStr) + lineStr = fmt.Sprintf("%s%s%s%s\n", prefix, nameStr, gstr.Repeat(" ", spaceLength+4), option.Brief) + wordwrapPrefix = gstr.Repeat(" ", len(prefix+nameStr)+spaceLength+4) + ) + lineStr = gstr.WordWrap(lineStr, maxLineChars, "\n"+wordwrapPrefix) + buffer.WriteString(lineStr) + } + buffer.WriteString("\n") + } + + // Example. + if c.Examples != "" { + buffer.WriteString("EXAMPLE\n") + buffer.WriteString(prefix) + buffer.WriteString(gstr.WordWrap(gstr.Trim(c.Examples), maxLineChars, "\n"+prefix)) + buffer.WriteString("\n") + } + + // Description. + if c.Description != "" { + buffer.WriteString("DESCRIPTION\n") + buffer.WriteString(prefix) + buffer.WriteString(gstr.WordWrap(gstr.Trim(c.Description), maxLineChars, "\n"+prefix)) + buffer.WriteString("\n") + } + + // Additional. + if c.Additional != "" { + lineStr := gstr.WordWrap(gstr.Trim(c.Additional), maxLineChars, "\n") + buffer.WriteString(lineStr) + buffer.WriteString("\n") + } + + fmt.Println(buffer.String()) +} + +func (c *Command) defaultHelpFunc(ctx context.Context, parser *Parser) error { + c.Print() + return nil +} diff --git a/os/gcmd/gcmd_command_run.go b/os/gcmd/gcmd_command_run.go new file mode 100644 index 000000000..391cf3c15 --- /dev/null +++ b/os/gcmd/gcmd_command_run.go @@ -0,0 +1,107 @@ +// 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 gcmd + +import ( + "context" + "fmt" + "os" + + "github.com/gogf/gf/v2/text/gstr" +) + +// Run calls custom function that bound to this command. +func (c *Command) Run(ctx context.Context) error { + // Parse command arguments and options using default algorithm. + parser, err := Parse(nil) + if err != nil { + return err + } + args := parser.GetArgAll() + if len(args) == 1 { + if c.HelpFunc != nil { + return c.HelpFunc(ctx, parser) + } + return c.defaultHelpFunc(ctx, parser) + } + + // Exclude the root binary name. + args = args[1:] + + // Find the matched command and run it. + if subCommand := c.searchCommand(args); subCommand != nil { + return subCommand.doRun(ctx, parser) + } + + // Print error and help command if no command found. + fmt.Printf( + "ERROR: command \"%s\" not found for arguments \"%s\"\n", + gstr.Join(args, " "), + gstr.Join(os.Args, " "), + ) + c.Print() + + return nil +} + +func (c *Command) doRun(ctx context.Context, parser *Parser) (err error) { + // Add built-in help option, just for info only. + c.Options = append(c.Options, defaultHelpOption) + // Check built-in help command. + if parser.ContainsOpt(helpOptionName) || parser.ContainsOpt(helpOptionNameShort) { + if c.HelpFunc != nil { + return c.HelpFunc(ctx, parser) + } + return c.defaultHelpFunc(ctx, parser) + } + // Reparse the arguments for current command configuration. + parser, err = c.reParse(ctx, parser) + if err != nil { + return err + } + // Registered command function calling. + return c.Func(ctx, parser) +} + +// reParse re-parses the arguments using option configuration of current command. +func (c *Command) reParse(ctx context.Context, parser *Parser) (*Parser, error) { + // It seems just has built-in help option, it so does nothing. + if len(c.Options) == 1 { + return parser, nil + } + var ( + optionKey string + supportedOptions = make(map[string]bool) + ) + for _, option := range c.Options { + if option.Short != "" { + optionKey = fmt.Sprintf(`%s.%s`, option.Name, option.Short) + } else { + optionKey = option.Name + } + supportedOptions[optionKey] = option.NeedValue + } + return Parse(supportedOptions) +} + +// searchCommand recursively searches the command according given arguments. +func (c *Command) searchCommand(args []string) *Command { + if len(args) == 0 { + return nil + } + for _, cmd := range c.commands { + if cmd.Name == args[0] { + leftArgs := args[1:] + if len(leftArgs) == 0 { + return &cmd + } + return cmd.searchCommand(leftArgs) + } + } + return nil +} diff --git a/os/gcmd/gcmd_handler.go b/os/gcmd/gcmd_handler.go deleted file mode 100644 index b371059c8..000000000 --- a/os/gcmd/gcmd_handler.go +++ /dev/null @@ -1,59 +0,0 @@ -// 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 gcmd - -import ( - "github.com/gogf/gf/v2/errors/gcode" - "github.com/gogf/gf/v2/errors/gerror" -) - -// BindHandle registers callback function `f` with `cmd`. -func BindHandle(cmd string, f func()) error { - if _, ok := defaultCommandFuncMap[cmd]; ok { - return gerror.NewCode(gcode.CodeInvalidOperation, "duplicated handle for command:"+cmd) - } else { - defaultCommandFuncMap[cmd] = f - } - return nil -} - -// BindHandleMap registers callback function with map `m`. -func BindHandleMap(m map[string]func()) error { - var err error - for k, v := range m { - if err = BindHandle(k, v); err != nil { - return err - } - } - return err -} - -// RunHandle executes the callback function registered by `cmd`. -func RunHandle(cmd string) error { - if handle, ok := defaultCommandFuncMap[cmd]; ok { - handle() - } else { - return gerror.NewCode(gcode.CodeMissingConfiguration, "no handle found for command:"+cmd) - } - return nil -} - -// AutoRun automatically recognizes and executes the callback function -// by value of index 0 (the first console parameter). -func AutoRun() error { - if cmd := GetArg(1); !cmd.IsEmpty() { - if handle, ok := defaultCommandFuncMap[cmd.String()]; ok { - handle() - } else { - return gerror.NewCode(gcode.CodeMissingConfiguration, "no handle found for command:"+cmd.String()) - } - } else { - return gerror.NewCode(gcode.CodeMissingParameter, "no command found") - } - return nil -} diff --git a/os/gcmd/gcmd_parser.go b/os/gcmd/gcmd_parser.go index 9306be1b9..746381c9a 100644 --- a/os/gcmd/gcmd_parser.go +++ b/os/gcmd/gcmd_parser.go @@ -14,6 +14,7 @@ import ( "github.com/gogf/gf/v2/container/gvar" "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/internal/command" "github.com/gogf/gf/v2/internal/json" "github.com/gogf/gf/v2/text/gregex" "github.com/gogf/gf/v2/text/gstr" @@ -25,7 +26,7 @@ type Parser struct { parsedArgs []string // As name described. parsedOptions map[string]string // As name described. passedOptions map[string]bool // User passed supported options. - supportedOptions map[string]bool // Option [option name : need argument]. + supportedOptions map[string]bool // Option [OptionName:WhetherNeedArgument]. commandFuncMap map[string]func() // Command function map for function handler. } @@ -36,6 +37,13 @@ type Parser struct { // // The optional parameter `strict` specifies whether stops parsing and returns error if invalid option passed. func Parse(supportedOptions map[string]bool, strict ...bool) (*Parser, error) { + if supportedOptions == nil { + command.Init(os.Args...) + return &Parser{ + parsedArgs: GetArgAll(), + parsedOptions: GetOptAll(), + }, nil + } return ParseWithArgs(os.Args, supportedOptions, strict...) } @@ -46,6 +54,13 @@ func Parse(supportedOptions map[string]bool, strict ...bool) (*Parser, error) { // // The optional parameter `strict` specifies whether stops parsing and returns error if invalid option passed. func ParseWithArgs(args []string, supportedOptions map[string]bool, strict ...bool) (*Parser, error) { + if supportedOptions == nil { + command.Init(args...) + return &Parser{ + parsedArgs: GetArgAll(), + parsedOptions: GetOptAll(), + }, nil + } strictParsing := false if len(strict) > 0 { strictParsing = strict[0] @@ -86,7 +101,7 @@ func ParseWithArgs(args []string, supportedOptions map[string]bool, strict ...bo } } else { // Multiple options? - if array := parser.parseMultiOption(option); len(array) > 0 { + if array = parser.parseMultiOption(option); len(array) > 0 { for _, v := range array { parser.setOptionValue(v, "") } diff --git a/os/gcmd/gcmd_z_unit_test.go b/os/gcmd/gcmd_z_unit_test.go index dc570e3c2..02b2f6673 100644 --- a/os/gcmd/gcmd_z_unit_test.go +++ b/os/gcmd/gcmd_z_unit_test.go @@ -9,10 +9,13 @@ package gcmd_test import ( + "context" + "fmt" "testing" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gcmd" + "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/genv" "github.com/gogf/gf/v2/test/gtest" ) @@ -67,3 +70,151 @@ func Test_GetWithEnv(t *testing.T) { t.Assert(gcmd.GetOptWithEnv("test"), 2) }) } + +func Test_Command(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + ctx = gctx.New() + err error + ) + commandRoot := &gcmd.Command{ + Name: "gf", + } + // env + commandEnv := gcmd.Command{ + Name: "env", + Func: func(ctx context.Context, parser *gcmd.Parser) error { + fmt.Println("env") + return nil + }, + } + // test + commandTest := gcmd.Command{ + Name: "test", + Brief: "test brief", + Description: "test description current Golang environment variables", + Examples: ` +gf get github.com/gogf/gf +gf get github.com/gogf/gf@latest +gf get github.com/gogf/gf@master +gf get golang.org/x/sys +`, + Options: []gcmd.Option{ + { + Name: "my-option", + Short: "o", + Brief: "It's my custom option", + NeedValue: false, + }, + { + Name: "another", + Short: "a", + Brief: "It's my another custom option", + NeedValue: false, + }, + }, + Func: func(ctx context.Context, parser *gcmd.Parser) error { + fmt.Println("test") + return nil + }, + } + err = commandRoot.Add( + commandEnv, + commandTest, + ) + if err != nil { + g.Log().Fatal(ctx, err) + } + + if err = commandRoot.Run(ctx); err != nil { + g.Log().Fatal(ctx, err) + } + }) +} + +func Test_Command_Print(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + ctx = gctx.New() + err error + ) + c := &gcmd.Command{ + Name: "gf", + Description: `GoFrame Command Line Interface, which is your helpmate for building GoFrame application with convenience.`, + Additional: ` +Use 'gf help COMMAND' or 'gf COMMAND -h' for detail about a command, which has '...' in the tail of their comments.`, + } + // env + commandEnv := gcmd.Command{ + Name: "env", + Brief: "show current Golang environment variables, long brief.long brief.long brief.long brief.long brief.long brief.long brief.long brief.", + Description: "show current Golang environment variables", + Func: func(ctx context.Context, parser *gcmd.Parser) error { + return nil + }, + } + if err = c.Add(commandEnv); err != nil { + g.Log().Fatal(ctx, err) + } + // get + commandGet := gcmd.Command{ + Name: "get", + Brief: "install or update GF to system in default...", + Description: "show current Golang environment variables", + + Examples: ` +gf get github.com/gogf/gf +gf get github.com/gogf/gf@latest +gf get github.com/gogf/gf@master +gf get golang.org/x/sys +`, + Func: func(ctx context.Context, parser *gcmd.Parser) error { + return nil + }, + } + if err = c.Add(commandGet); err != nil { + g.Log().Fatal(ctx, err) + } + // build + //-n, --name output binary name + //-v, --version output binary version + //-a, --arch output binary architecture, multiple arch separated with ',' + //-s, --system output binary system, multiple os separated with ',' + //-o, --output output binary path, used when building single binary file + //-p, --path output binary directory path, default is './bin' + //-e, --extra extra custom "go build" options + //-m, --mod like "-mod" option of "go build", use "-m none" to disable go module + //-c, --cgo enable or disable cgo feature, it's disabled in default + + commandBuild := gcmd.Command{ + Name: "build", + Usage: "gf build FILE [OPTION]", + Brief: "cross-building go project for lots of platforms...", + Description: ` +The "build" command is most commonly used command, which is designed as a powerful wrapper for +"go build" command for convenience cross-compiling usage. +It provides much more features for building binary: +1. Cross-Compiling for many platforms and architectures. +2. Configuration file support for compiling. +3. Build-In Variables. +`, + Examples: ` +gf build main.go +gf build main.go --swagger +gf build main.go --pack public,template +gf build main.go --cgo +gf build main.go -m none +gf build main.go -n my-app -a all -s all +gf build main.go -n my-app -a amd64,386 -s linux -p . +gf build main.go -n my-app -v 1.0 -a amd64,386 -s linux,windows,darwin -p ./docker/bin +`, + Func: func(ctx context.Context, parser *gcmd.Parser) error { + return nil + }, + } + if err = c.Add(commandBuild); err != nil { + g.Log().Fatal(ctx, err) + } + c.Run(ctx) + }) +} diff --git a/os/gfile/gfile_home.go b/os/gfile/gfile_home.go index f75de43fe..68bc6c645 100644 --- a/os/gfile/gfile_home.go +++ b/os/gfile/gfile_home.go @@ -38,7 +38,7 @@ func getHomePath() (string, error) { if nil == err { return u.HomeDir, nil } - if "windows" == runtime.GOOS { + if runtime.GOOS == "windows" { return homeWindows() } return homeUnix() diff --git a/os/gfile/gfile_z_example_cache_test.go b/os/gfile/gfile_z_example_cache_test.go new file mode 100644 index 000000000..3ca819779 --- /dev/null +++ b/os/gfile/gfile_z_example_cache_test.go @@ -0,0 +1,43 @@ +// 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 gfile_test + +import ( + "fmt" + "time" + + "github.com/gogf/gf/v2/os/gfile" +) + +func ExampleGetContentsWithCache() { + // init + var ( + fileName = "gflie_example.txt" + tempDir = gfile.TempDir("gfile_example_cache") + tempFile = gfile.Join(tempDir, fileName) + ) + + // write contents + gfile.PutContents(tempFile, "goframe example content") + + // It reads the file content with cache duration of one minute, + // which means it reads from cache after then without any IO operations within on minute. + fmt.Println(gfile.GetContentsWithCache(tempFile, time.Minute)) + + // write new contents will clear its cache + gfile.PutContents(tempFile, "new goframe example content") + + // There's some delay for cache clearing after file content change. + time.Sleep(time.Second * 1) + + // read contents + fmt.Println(gfile.GetContentsWithCache(tempFile)) + + // May Output: + // goframe example content + // new goframe example content +} diff --git a/os/gfile/gfile_z_example_contents_test.go b/os/gfile/gfile_z_example_contents_test.go new file mode 100644 index 000000000..13e90500c --- /dev/null +++ b/os/gfile/gfile_z_example_contents_test.go @@ -0,0 +1,240 @@ +// 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 gfile_test + +import ( + "fmt" + + "github.com/gogf/gf/v2/os/gfile" +) + +func ExampleGetContents() { + // init + var ( + fileName = "gflie_example.txt" + tempDir = gfile.TempDir("gfile_example_content") + tempFile = gfile.Join(tempDir, fileName) + ) + + // write contents + gfile.PutContents(tempFile, "goframe example content") + + // It reads and returns the file content as string. + // It returns empty string if it fails reading, for example, with permission or IO error. + fmt.Println(gfile.GetContents(tempFile)) + + // Output: + // goframe example content +} + +func ExampleGetBytes() { + // init + var ( + fileName = "gflie_example.txt" + tempDir = gfile.TempDir("gfile_example_content") + tempFile = gfile.Join(tempDir, fileName) + ) + + // write contents + gfile.PutContents(tempFile, "goframe example content") + + // It reads and returns the file content as []byte. + // It returns nil if it fails reading, for example, with permission or IO error. + fmt.Println(gfile.GetBytes(tempFile)) + + // Output: + // [103 111 102 114 97 109 101 32 101 120 97 109 112 108 101 32 99 111 110 116 101 110 116] +} + +func ExamplePutContents() { + // init + var ( + fileName = "gflie_example.txt" + tempDir = gfile.TempDir("gfile_example_content") + tempFile = gfile.Join(tempDir, fileName) + ) + + // It creates and puts content string into specifies file path. + // It automatically creates directory recursively if it does not exist. + gfile.PutContents(tempFile, "goframe example content") + + // read contents + fmt.Println(gfile.GetContents(tempFile)) + + // Output: + // goframe example content +} + +func ExamplePutBytes() { + // init + var ( + fileName = "gflie_example.txt" + tempDir = gfile.TempDir("gfile_example_content") + tempFile = gfile.Join(tempDir, fileName) + ) + + // write contents + gfile.PutBytes(tempFile, []byte("goframe example content")) + + // read contents + fmt.Println(gfile.GetContents(tempFile)) + + // Output: + // goframe example content +} + +func ExamplePutContentsAppend() { + // init + var ( + fileName = "gflie_example.txt" + tempDir = gfile.TempDir("gfile_example_content") + tempFile = gfile.Join(tempDir, fileName) + ) + + // write contents + gfile.PutContents(tempFile, "goframe example content") + + // read contents + fmt.Println(gfile.GetContents(tempFile)) + + // It creates and append content string into specifies file path. + // It automatically creates directory recursively if it does not exist. + gfile.PutContentsAppend(tempFile, " append content") + + // read contents + fmt.Println(gfile.GetContents(tempFile)) + + // Output: + // goframe example content + // goframe example content append content +} + +func ExamplePutBytesAppend() { + // init + var ( + fileName = "gflie_example.txt" + tempDir = gfile.TempDir("gfile_example_content") + tempFile = gfile.Join(tempDir, fileName) + ) + + // write contents + gfile.PutContents(tempFile, "goframe example content") + + // read contents + fmt.Println(gfile.GetContents(tempFile)) + + // write contents + gfile.PutBytesAppend(tempFile, []byte(" append")) + + // read contents + fmt.Println(gfile.GetContents(tempFile)) + + // Output: + // goframe example content + // goframe example content append +} + +func ExampleGetNextCharOffsetByPath() { + // init + var ( + fileName = "gflie_example.txt" + tempDir = gfile.TempDir("gfile_example_content") + tempFile = gfile.Join(tempDir, fileName) + ) + + // write contents + gfile.PutContents(tempFile, "goframe example content") + + // read contents + index := gfile.GetNextCharOffsetByPath(tempFile, 'f', 0) + fmt.Println(index) + + // Output: + // 2 +} + +func ExampleGetBytesTilCharByPath() { + // init + var ( + fileName = "gflie_example.txt" + tempDir = gfile.TempDir("gfile_example_content") + tempFile = gfile.Join(tempDir, fileName) + ) + + // write contents + gfile.PutContents(tempFile, "goframe example content") + + // read contents + fmt.Println(gfile.GetBytesTilCharByPath(tempFile, 'f', 0)) + + // Output: + // [103 111 102] 2 +} + +func ExampleGetBytesByTwoOffsetsByPath() { + // init + var ( + fileName = "gflie_example.txt" + tempDir = gfile.TempDir("gfile_example_content") + tempFile = gfile.Join(tempDir, fileName) + ) + + // write contents + gfile.PutContents(tempFile, "goframe example content") + + // read contents + fmt.Println(gfile.GetBytesByTwoOffsetsByPath(tempFile, 0, 7)) + + // Output: + // [103 111 102 114 97 109 101] +} + +func ExampleReadLines() { + // init + var ( + fileName = "gflie_example.txt" + tempDir = gfile.TempDir("gfile_example_content") + tempFile = gfile.Join(tempDir, fileName) + ) + + // write contents + gfile.PutContents(tempFile, "L1 goframe example content\nL2 goframe example content") + + // read contents + gfile.ReadLines(tempFile, func(text string) error { + // Process each line + fmt.Println(text) + return nil + }) + + // Output: + // L1 goframe example content + // L2 goframe example content +} + +func ExampleReadLinesBytes() { + // init + var ( + fileName = "gflie_example.txt" + tempDir = gfile.TempDir("gfile_example_content") + tempFile = gfile.Join(tempDir, fileName) + ) + + // write contents + gfile.PutContents(tempFile, "L1 goframe example content\nL2 goframe example content") + + // read contents + gfile.ReadLinesBytes(tempFile, func(bytes []byte) error { + // Process each line + fmt.Println(bytes) + return nil + }) + + // Output: + // [76 49 32 103 111 102 114 97 109 101 32 101 120 97 109 112 108 101 32 99 111 110 116 101 110 116] + // [76 50 32 103 111 102 114 97 109 101 32 101 120 97 109 112 108 101 32 99 111 110 116 101 110 116] +} diff --git a/os/gfile/gfile_z_example_copy_test.go b/os/gfile/gfile_z_example_copy_test.go new file mode 100644 index 000000000..293d40a82 --- /dev/null +++ b/os/gfile/gfile_z_example_copy_test.go @@ -0,0 +1,52 @@ +// 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 gfile_test + +import ( + "fmt" + + "github.com/gogf/gf/v2/os/gfile" +) + +func ExampleCopy() { + // init + var ( + srcFileName = "gflie_example.txt" + srcTempDir = gfile.TempDir("gfile_example_copy_src") + srcTempFile = gfile.Join(srcTempDir, srcFileName) + + // copy file + dstFileName = "gflie_example_copy.txt" + dstTempFile = gfile.Join(srcTempDir, dstFileName) + + // copy dir + dstTempDir = gfile.TempDir("gfile_example_copy_dst") + ) + + // write contents + gfile.PutContents(srcTempFile, "goframe example copy") + + // copy file + gfile.Copy(srcTempFile, dstTempFile) + + // read contents after copy file + fmt.Println(gfile.GetContents(dstTempFile)) + + // copy dir + gfile.Copy(srcTempDir, dstTempDir) + + // list copy dir file + fList, _ := gfile.ScanDir(dstTempDir, "*", false) + for _, v := range fList { + fmt.Println(gfile.Basename(v)) + } + + // Output: + // goframe example copy + // gflie_example.txt + // gflie_example_copy.txt +} diff --git a/os/gfile/gfile_z_example_home_test.go b/os/gfile/gfile_z_example_home_test.go new file mode 100644 index 000000000..9cc2b9707 --- /dev/null +++ b/os/gfile/gfile_z_example_home_test.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 gfile_test + +import ( + "fmt" + + "github.com/gogf/gf/v2/os/gfile" +) + +func ExampleHome() { + // user's home directory + homePath, _ := gfile.Home() + fmt.Println(homePath) + + // May Output: + // C:\Users\hailaz +} diff --git a/os/gfile/gfile_z_example_replace_test.go b/os/gfile/gfile_z_example_replace_test.go new file mode 100644 index 000000000..df4d3a931 --- /dev/null +++ b/os/gfile/gfile_z_example_replace_test.go @@ -0,0 +1,120 @@ +// 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 gfile_test + +import ( + "fmt" + "regexp" + + "github.com/gogf/gf/v2/os/gfile" +) + +func ExampleReplaceFile() { + // init + var ( + fileName = "gflie_example.txt" + tempDir = gfile.TempDir("gfile_example_replace") + tempFile = gfile.Join(tempDir, fileName) + ) + + // write contents + gfile.PutContents(tempFile, "goframe example content") + + // read contents + fmt.Println(gfile.GetContents(tempFile)) + + // It replaces content directly by file path. + gfile.ReplaceFile("content", "replace word", tempFile) + + fmt.Println(gfile.GetContents(tempFile)) + + // Output: + // goframe example content + // goframe example replace word +} + +func ExampleReplaceFileFunc() { + // init + var ( + fileName = "gflie_example.txt" + tempDir = gfile.TempDir("gfile_example_replace") + tempFile = gfile.Join(tempDir, fileName) + ) + + // write contents + gfile.PutContents(tempFile, "goframe example 123") + + // read contents + fmt.Println(gfile.GetContents(tempFile)) + + // It replaces content directly by file path and callback function. + gfile.ReplaceFileFunc(func(path, content string) string { + // Replace with regular match + reg, _ := regexp.Compile(`\d{3}`) + return reg.ReplaceAllString(content, "[num]") + }, tempFile) + + fmt.Println(gfile.GetContents(tempFile)) + + // Output: + // goframe example 123 + // goframe example [num] +} + +func ExampleReplaceDir() { + // init + var ( + fileName = "gflie_example.txt" + tempDir = gfile.TempDir("gfile_example_replace") + tempFile = gfile.Join(tempDir, fileName) + ) + + // write contents + gfile.PutContents(tempFile, "goframe example content") + + // read contents + fmt.Println(gfile.GetContents(tempFile)) + + // It replaces content of all files under specified directory recursively. + gfile.ReplaceDir("content", "replace word", tempDir, "gflie_example.txt", true) + + // read contents + fmt.Println(gfile.GetContents(tempFile)) + + // Output: + // goframe example content + // goframe example replace word +} + +func ExampleReplaceDirFunc() { + // init + var ( + fileName = "gflie_example.txt" + tempDir = gfile.TempDir("gfile_example_replace") + tempFile = gfile.Join(tempDir, fileName) + ) + + // write contents + gfile.PutContents(tempFile, "goframe example 123") + + // read contents + fmt.Println(gfile.GetContents(tempFile)) + + // It replaces content of all files under specified directory with custom callback function recursively. + gfile.ReplaceDirFunc(func(path, content string) string { + // Replace with regular match + reg, _ := regexp.Compile(`\d{3}`) + return reg.ReplaceAllString(content, "[num]") + }, tempDir, "gflie_example.txt", true) + + fmt.Println(gfile.GetContents(tempFile)) + + // Output: + // goframe example 123 + // goframe example [num] + +} diff --git a/os/gfile/gfile_z_example_scan_test.go b/os/gfile/gfile_z_example_scan_test.go new file mode 100644 index 000000000..0238dbbb0 --- /dev/null +++ b/os/gfile/gfile_z_example_scan_test.go @@ -0,0 +1,133 @@ +// 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 gfile_test + +import ( + "fmt" + + "github.com/gogf/gf/v2/os/gfile" +) + +func ExampleScanDir() { + // init + var ( + fileName = "gflie_example.txt" + tempDir = gfile.TempDir("gfile_example_scan_dir") + tempFile = gfile.Join(tempDir, fileName) + + tempSubDir = gfile.Join(tempDir, "sub_dir") + tempSubFile = gfile.Join(tempSubDir, fileName) + ) + + // write contents + gfile.PutContents(tempFile, "goframe example content") + gfile.PutContents(tempSubFile, "goframe example content") + + // scans directory recursively + list, _ := gfile.ScanDir(tempDir, "*", true) + for _, v := range list { + fmt.Println(gfile.Basename(v)) + } + + // Output: + // gflie_example.txt + // sub_dir + // gflie_example.txt +} + +func ExampleScanDirFile() { + // init + var ( + fileName = "gflie_example.txt" + tempDir = gfile.TempDir("gfile_example_scan_dir_file") + tempFile = gfile.Join(tempDir, fileName) + + tempSubDir = gfile.Join(tempDir, "sub_dir") + tempSubFile = gfile.Join(tempSubDir, fileName) + ) + + // write contents + gfile.PutContents(tempFile, "goframe example content") + gfile.PutContents(tempSubFile, "goframe example content") + + // scans directory recursively exclusive of directories + list, _ := gfile.ScanDirFile(tempDir, "*.txt", true) + for _, v := range list { + fmt.Println(gfile.Basename(v)) + } + + // Output: + // gflie_example.txt + // gflie_example.txt +} + +func ExampleScanDirFunc() { + // init + var ( + fileName = "gflie_example.txt" + tempDir = gfile.TempDir("gfile_example_scan_dir_func") + tempFile = gfile.Join(tempDir, fileName) + + tempSubDir = gfile.Join(tempDir, "sub_dir") + tempSubFile = gfile.Join(tempSubDir, fileName) + ) + + // write contents + gfile.PutContents(tempFile, "goframe example content") + gfile.PutContents(tempSubFile, "goframe example content") + + // scans directory recursively + list, _ := gfile.ScanDirFunc(tempDir, "*", true, func(path string) string { + // ignores some files + if gfile.Basename(path) == "gflie_example.txt" { + return "" + } + return path + }) + for _, v := range list { + fmt.Println(gfile.Basename(v)) + } + + // Output: + // sub_dir +} + +func ExampleScanDirFileFunc() { + // init + var ( + fileName = "gflie_example.txt" + tempDir = gfile.TempDir("gfile_example_scan_dir_file_func") + tempFile = gfile.Join(tempDir, fileName) + + fileName1 = "gflie_example_ignores.txt" + tempFile1 = gfile.Join(tempDir, fileName1) + + tempSubDir = gfile.Join(tempDir, "sub_dir") + tempSubFile = gfile.Join(tempSubDir, fileName) + ) + + // write contents + gfile.PutContents(tempFile, "goframe example content") + gfile.PutContents(tempFile1, "goframe example content") + gfile.PutContents(tempSubFile, "goframe example content") + + // scans directory recursively exclusive of directories + list, _ := gfile.ScanDirFileFunc(tempDir, "*.txt", true, func(path string) string { + // ignores some files + if gfile.Basename(path) == "gflie_example_ignores.txt" { + return "" + } + return path + }) + for _, v := range list { + fmt.Println(gfile.Basename(v)) + } + + // Output: + // gflie_example.txt + // gflie_example.txt +} diff --git a/os/gfile/gfile_z_example_search_test.go b/os/gfile/gfile_z_example_search_test.go new file mode 100644 index 000000000..3fdaf4309 --- /dev/null +++ b/os/gfile/gfile_z_example_search_test.go @@ -0,0 +1,32 @@ +// 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 gfile_test + +import ( + "fmt" + + "github.com/gogf/gf/v2/os/gfile" +) + +func ExampleSearch() { + // init + var ( + fileName = "gflie_example.txt" + tempDir = gfile.TempDir("gfile_example_search") + tempFile = gfile.Join(tempDir, fileName) + ) + + // write contents + gfile.PutContents(tempFile, "goframe example content") + + // search file + realPath, _ := gfile.Search(fileName, tempDir) + fmt.Println(gfile.Basename(realPath)) + + // Output: + // gflie_example.txt +} diff --git a/os/gfile/gfile_z_example_size_test.go b/os/gfile/gfile_z_example_size_test.go new file mode 100644 index 000000000..b4696e6bc --- /dev/null +++ b/os/gfile/gfile_z_example_size_test.go @@ -0,0 +1,83 @@ +// 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 gfile_test + +import ( + "fmt" + + "github.com/gogf/gf/v2/os/gfile" +) + +func ExampleSize() { + // init + var ( + fileName = "gflie_example.txt" + tempDir = gfile.TempDir("gfile_example_size") + tempFile = gfile.Join(tempDir, fileName) + ) + + // write contents + gfile.PutContents(tempFile, "0123456789") + fmt.Println(gfile.Size(tempFile)) + + // Output: + // 10 +} + +func ExampleSizeFormat() { + // init + var ( + fileName = "gflie_example.txt" + tempDir = gfile.TempDir("gfile_example_size") + tempFile = gfile.Join(tempDir, fileName) + ) + + // write contents + gfile.PutContents(tempFile, "0123456789") + fmt.Println(gfile.SizeFormat(tempFile)) + + // Output: + // 10.00B +} + +func ExampleReadableSize() { + // init + var ( + fileName = "gflie_example.txt" + tempDir = gfile.TempDir("gfile_example_size") + tempFile = gfile.Join(tempDir, fileName) + ) + + // write contents + gfile.PutContents(tempFile, "01234567899876543210") + fmt.Println(gfile.ReadableSize(tempFile)) + + // Output: + // 20.00B +} + +func ExampleStrToSize() { + size := gfile.StrToSize("100MB") + fmt.Println(size) + + // Output: + // 104857600 +} + +func ExampleFormatSize() { + sizeStr := gfile.FormatSize(104857600) + fmt.Println(sizeStr) + sizeStr0 := gfile.FormatSize(1024) + fmt.Println(sizeStr0) + sizeStr1 := gfile.FormatSize(999999999999999999) + fmt.Println(sizeStr1) + + // Output: + // 100.00M + // 1.00K + // 888.18P +} diff --git a/os/gfile/gfile_z_example_sort_test.go b/os/gfile/gfile_z_example_sort_test.go new file mode 100644 index 000000000..aaa46d2b6 --- /dev/null +++ b/os/gfile/gfile_z_example_sort_test.go @@ -0,0 +1,32 @@ +// 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 gfile_test + +import ( + "fmt" + + "github.com/gogf/gf/v2/os/gfile" +) + +func ExampleSortFiles() { + files := []string{ + "/aaa/bbb/ccc.txt", + "/aaa/bbb/", + "/aaa/", + "/aaa", + "/aaa/ccc/ddd.txt", + "/bbb", + "/0123", + "/ddd", + "/ccc", + } + sortOut := gfile.SortFiles(files) + fmt.Println(sortOut) + + // Output: + // [/0123 /aaa /aaa/ /aaa/bbb/ /aaa/bbb/ccc.txt /aaa/ccc/ddd.txt /bbb /ccc /ddd] +} diff --git a/os/gfile/gfile_z_example_time_test.go b/os/gfile/gfile_z_example_time_test.go new file mode 100644 index 000000000..e4a11ef51 --- /dev/null +++ b/os/gfile/gfile_z_example_time_test.go @@ -0,0 +1,37 @@ +// 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 gfile_test + +import ( + "fmt" + + "github.com/gogf/gf/v2/os/gfile" +) + +func ExampleMTime() { + t := gfile.MTime(gfile.TempDir()) + fmt.Println(t) + + // May Output: + // 2021-11-02 15:18:43.901141 +0800 CST +} + +func ExampleMTimestamp() { + t := gfile.MTimestamp(gfile.TempDir()) + fmt.Println(t) + + // May Output: + // 1635838398 +} + +func ExampleMTimestampMilli() { + t := gfile.MTimestampMilli(gfile.TempDir()) + fmt.Println(t) + + // May Output: + // 1635838529330 +} diff --git a/os/gfile/gfile_z_unit_contents_test.go b/os/gfile/gfile_z_unit_contents_test.go index bd972d523..3d38e18a6 100644 --- a/os/gfile/gfile_z_unit_contents_test.go +++ b/os/gfile/gfile_z_unit_contents_test.go @@ -103,8 +103,8 @@ func Test_Truncate(t *testing.T) { t.Assert(err, nil) files, err = os.Open(testpath() + filepaths1) - defer files.Close() t.Assert(err, nil) + defer files.Close() fileinfo, err2 := files.Stat() t.Assert(err2, nil) t.Assert(fileinfo.Size(), 10) diff --git a/os/gfile/gfile_z_unit_test.go b/os/gfile/gfile_z_unit_test.go index 60d7bf7ba..4ef155e26 100644 --- a/os/gfile/gfile_z_unit_test.go +++ b/os/gfile/gfile_z_unit_test.go @@ -190,7 +190,7 @@ func Test_OpenWithFlagPerm(t *testing.T) { flags = append(flags, false) for k, v := range files { - fileobj, err = gfile.OpenWithFlagPerm(testpath()+v, os.O_RDWR, 666) + fileobj, err = gfile.OpenWithFlagPerm(testpath()+v, os.O_RDWR, 0666) fileobj.Close() if flags[k] { t.Assert(err, nil) diff --git a/util/gvalid/gvalid.go b/util/gvalid/gvalid.go index 5c358ee9f..d2b816459 100644 --- a/util/gvalid/gvalid.go +++ b/util/gvalid/gvalid.go @@ -15,56 +15,6 @@ import ( "github.com/gogf/gf/v2/text/gregex" ) -// Refer to Laravel validation: -// https://laravel.com/docs/5.5/validation#available-validation-rules -// https://learnku.com/docs/laravel/5.4/validation -// -// All supported rules: -// required format: required brief: Required. -// required-if format: required-if:field,value,... brief: Required unless all given field and its value are equal. -// required-unless format: required-unless:field,value,... brief: Required unless all given field and its value are not equal. -// required-with format: required-with:field1,field2,... brief: Required if any of given fields are not empty. -// required-with-all format: required-with-all:field1,field2,... brief: Required if all given fields are not empty. -// required-without format: required-without:field1,field2,... brief: Required if any of given fields are empty. -// required-without-all format: required-without-all:field1,field2,...brief: Required if all given fields are empty. -// bail format: bail brief: Stop validating when this field's validation failed. -// date format: date brief: Standard date, like: 2006-01-02, 20060102, 2006.01.02 -// datetime format: datetime brief: Standard datetime, like: 2006-01-02 12:00:00 -// date-format format: date-format:format brief: Custom date format. -// email format: email brief: Email address. -// phone format: phone brief: Phone number. -// telephone format: telephone brief: Telephone number, like: "XXXX-XXXXXXX"、"XXXX-XXXXXXXX"、"XXX-XXXXXXX"、"XXX-XXXXXXXX"、"XXXXXXX"、"XXXXXXXX" -// passport format: passport brief: Universal passport format rule: Starting with letter, containing only numbers or underscores, length between 6 and 18 -// password format: password brief: Universal password format rule1: Containing any visible chars, length between 6 and 18. -// password2 format: password2 brief: Universal password format rule2: Must meet password rule1, must contain lower and upper letters and numbers. -// password3 format: password3 brief: Universal password format rule3: Must meet password rule1, must contain lower and upper letters, numbers and special chars. -// postcode format: postcode brief: Postcode number. -// resident-id format: resident-id brief: Resident id number. -// bank-card format: bank-card brief: Bank card nunber. -// qq format: qq brief: Tencent QQ number. -// ip format: ip brief: IPv4/IPv6. -// ipv4 format: ipv4 brief: IPv4. -// ipv6 format: ipv6 brief: IPv6. -// mac format: mac brief: MAC. -// url format: url brief: URL. -// domain format: domain brief: Domain. -// length format: length:min,max brief: Length between :min and :max. The length is calculated using unicode string, which means one chinese character or letter both has the length of 1. -// min-length format: min-length:min brief: Length is equal or greater than :min. The length is calculated using unicode string, which means one chinese character or letter both has the length of 1. -// max-length format: max-length:max brief: Length is equal or lesser than :max. The length is calculated using unicode string, which means one chinese character or letter both has the length of 1. -// size format: size:size brief: Length must be :size. The length is calculated using unicode string, which means one chinese character or letter both has the length of 1. -// between format: between:min,max brief: Range between :min and :max. It supports both integer and float. -// min format: min:min brief: Equal or greater than :min. It supports both integer and float. -// max format: max:max brief: Equal or lesser than :max. It supports both integer and float. -// json format: json brief: JSON. -// integer format: integer brief: Integer. -// float format: float brief: Float. Note that an integer is actually a float number. -// boolean format: boolean brief: Boolean(1,true,on,yes:true | 0,false,off,no,"":false) -// same format: same:field brief: Value should be the same as value of field. -// different format: different:field brief: Value should be different from value of field. -// in format: in:value1,value2,... brief: Value should be in: value1,value2,... -// not-in format: not-in:value1,value2,... brief: Value should not be in: value1,value2,... -// regex format: regex:pattern brief: Value should match custom regular expression pattern. - // CustomMsg is the custom error message type, // like: map[field] => string|map[rule]string type CustomMsg = map[string]interface{} @@ -89,100 +39,64 @@ const ( internalDefaultRuleName = "__default__" // default rule name for i18n error message format if no i18n message found for specified error rule. ruleMessagePrefixForI18n = "gf.gvalid.rule." // prefix string for each rule configuration in i18n content. noValidationTagName = "nv" // no validation tag name for struct attribute. - bailRuleName = "bail" // the name for rule "bail" + ruleNameBail = "bail" // the name for rule "bail" + ruleNameCi = "ci" // the name for rule "ci" ) var ( - defaultValidator = New() // defaultValidator is the default validator for package functions. - structTagPriority = []string{"gvalid", "valid", "v"} // structTagPriority specifies the validation tag priority array. - aliasNameTagPriority = []string{"param", "params", "p"} // aliasNameTagPriority specifies the alias tag priority array. - - // all internal error keys. - internalErrKeyMap = map[string]string{ - internalRulesErrRuleName: internalRulesErrRuleName, - internalParamsErrRuleName: internalParamsErrRuleName, - internalObjectErrRuleName: internalObjectErrRuleName, - } - // regular expression object for single rule - // which is compiled just once and of repeatable usage. - ruleRegex, _ = regexp.Compile(singleRulePattern) - - // mustCheckRulesEvenValueEmpty specifies some rules that must be validated - // even the value is empty (nil or empty). - mustCheckRulesEvenValueEmpty = map[string]struct{}{ - "required": {}, - "required-if": {}, - "required-unless": {}, - "required-with": {}, - "required-with-all": {}, - "required-without": {}, - "required-without-all": {}, - //"same": {}, - //"different": {}, - //"in": {}, - //"not-in": {}, - //"regex": {}, - } // allSupportedRules defines all supported rules that is used for quick checks. + // Refer to Laravel validation: + // https://laravel.com/docs/5.5/validation#available-validation-rules + // https://learnku.com/docs/laravel/5.4/validation allSupportedRules = map[string]struct{}{ - "required": {}, - "required-if": {}, - "required-unless": {}, - "required-with": {}, - "required-with-all": {}, - "required-without": {}, - "required-without-all": {}, - "bail": {}, - "date": {}, - "datetime": {}, - "date-format": {}, - "email": {}, - "phone": {}, - "phone-loose": {}, - "telephone": {}, - "passport": {}, - "password": {}, - "password2": {}, - "password3": {}, - "postcode": {}, - "resident-id": {}, - "bank-card": {}, - "qq": {}, - "ip": {}, - "ipv4": {}, - "ipv6": {}, - "mac": {}, - "url": {}, - "domain": {}, - "length": {}, - "min-length": {}, - "max-length": {}, - "size": {}, - "between": {}, - "min": {}, - "max": {}, - "json": {}, - "integer": {}, - "float": {}, - "boolean": {}, - "same": {}, - "different": {}, - "in": {}, - "not-in": {}, - "regex": {}, - } - // boolMap defines the boolean values. - boolMap = map[string]struct{}{ - "1": {}, - "true": {}, - "on": {}, - "yes": {}, - "": {}, - "0": {}, - "false": {}, - "off": {}, - "no": {}, + "required": {}, // format: required brief: Required. + "required-if": {}, // format: required-if:field,value,... brief: Required unless all given field and its value are equal. + "required-unless": {}, // format: required-unless:field,value,... brief: Required unless all given field and its value are not equal. + "required-with": {}, // format: required-with:field1,field2,... brief: Required if any of given fields are not empty. + "required-with-all": {}, // format: required-with-all:field1,field2,... brief: Required if all given fields are not empty. + "required-without": {}, // format: required-without:field1,field2,... brief: Required if any of given fields are empty. + "required-without-all": {}, // format: required-without-all:field1,field2,...brief: Required if all given fields are empty. + "bail": {}, // format: bail brief: Stop validating when this field's validation failed. + "ci": {}, // format: ci brief: Case-Insensitive configuration for those rules that need value comparison like: same, different, in, not-in, etc. + "date": {}, // format: date brief: Standard date, like: 2006-01-02, 20060102, 2006.01.02 + "datetime": {}, // format: datetime brief: Standard datetime, like: 2006-01-02 12:00:00 + "date-format": {}, // format: date-format:format brief: Custom date format. + "email": {}, // format: email brief: Email address. + "phone": {}, // format: phone brief: Phone number. + "phone-loose": {}, // format: phone-loose brief: Loose phone number validation. + "telephone": {}, // format: telephone brief: Telephone number, like: "XXXX-XXXXXXX"、"XXXX-XXXXXXXX"、"XXX-XXXXXXX"、"XXX-XXXXXXXX"、"XXXXXXX"、"XXXXXXXX" + "passport": {}, // format: passport brief: Universal passport format rule: Starting with letter, containing only numbers or underscores, length between 6 and 18 + "password": {}, // format: password brief: Universal password format rule1: Containing any visible chars, length between 6 and 18. + "password2": {}, // format: password2 brief: Universal password format rule2: Must meet password rule1, must contain lower and upper letters and numbers. + "password3": {}, // format: password3 brief: Universal password format rule3: Must meet password rule1, must contain lower and upper letters, numbers and special chars. + "postcode": {}, // format: postcode brief: Postcode number. + "resident-id": {}, // format: resident-id brief: Resident id number. + "bank-card": {}, // format: bank-card brief: Bank card number. + "qq": {}, // format: qq brief: Tencent QQ number. + "ip": {}, // format: ip brief: IPv4/IPv6. + "ipv4": {}, // format: ipv4 brief: IPv4. + "ipv6": {}, // format: ipv6 brief: IPv6. + "mac": {}, // format: mac brief: MAC. + "url": {}, // format: url brief: URL. + "domain": {}, // format: domain brief: Domain. + "length": {}, // format: length:min,max brief: Length between :min and :max. The length is calculated using unicode string, which means one chinese character or letter both has the length of 1. + "min-length": {}, // format: min-length:min brief: Length is equal or greater than :min. The length is calculated using unicode string, which means one chinese character or letter both has the length of 1. + "max-length": {}, // format: max-length:max brief: Length is equal or lesser than :max. The length is calculated using unicode string, which means one chinese character or letter both has the length of 1. + "size": {}, // format: size:size brief: Length must be :size. The length is calculated using unicode string, which means one chinese character or letter both has the length of 1. + "between": {}, // format: between:min,max brief: Range between :min and :max. It supports both integer and float. + "min": {}, // format: min:min brief: Equal or greater than :min. It supports both integer and float. + "max": {}, // format: max:max brief: Equal or lesser than :max. It supports both integer and float. + "json": {}, // format: json brief: JSON. + "integer": {}, // format: integer brief: Integer. + "float": {}, // format: float brief: Float. Note that an integer is actually a float number. + "boolean": {}, // format: boolean brief: Boolean(1,true,on,yes:true | 0,false,off,no,"":false) + "same": {}, // format: same:field brief: Value should be the same as value of field. + "different": {}, // format: different:field brief: Value should be different from value of field. + "in": {}, // format: in:value1,value2,... brief: Value should be in: value1,value2,... + "not-in": {}, // format: not-in:value1,value2,... brief: Value should not be in: value1,value2,... + "regex": {}, // format: regex:pattern brief: Value should match custom regular expression pattern. } + // defaultMessages is the default error messages. // Note that these messages are synchronized from ./i18n/en/validation.toml . defaultMessages = map[string]string{ @@ -232,11 +146,56 @@ var ( "regex": "The {attribute} value `{value}` must be in regex of: {pattern}", internalDefaultRuleName: "The {attribute} value `{value}` is invalid", } + + // mustCheckRulesEvenValueEmpty specifies some rules that must be validated + // even the value is empty (nil or empty). + mustCheckRulesEvenValueEmpty = map[string]struct{}{ + "required": {}, + "required-if": {}, + "required-unless": {}, + "required-with": {}, + "required-with-all": {}, + "required-without": {}, + "required-without-all": {}, + //"same": {}, + //"different": {}, + //"in": {}, + //"not-in": {}, + //"regex": {}, + } + + // boolMap defines the boolean values. + boolMap = map[string]struct{}{ + "1": {}, + "true": {}, + "on": {}, + "yes": {}, + "": {}, + "0": {}, + "false": {}, + "off": {}, + "no": {}, + } + + defaultValidator = New() // defaultValidator is the default validator for package functions. + structTagPriority = []string{"gvalid", "valid", "v"} // structTagPriority specifies the validation tag priority array. + aliasNameTagPriority = []string{"param", "params", "p"} // aliasNameTagPriority specifies the alias tag priority array. + + // all internal error keys. + internalErrKeyMap = map[string]string{ + internalRulesErrRuleName: internalRulesErrRuleName, + internalParamsErrRuleName: internalParamsErrRuleName, + internalObjectErrRuleName: internalObjectErrRuleName, + } + // regular expression object for single rule + // which is compiled just once and of repeatable usage. + ruleRegex, _ = regexp.Compile(singleRulePattern) + // markedRuleMap defines all rules that are just marked rules which have neither functional meaning // nor error messages. markedRuleMap = map[string]bool{ - bailRuleName: true, - //"nullable": true, + ruleNameBail: true, + ruleNameCi: true, } ) diff --git a/util/gvalid/gvalid_validator.go b/util/gvalid/gvalid_validator.go index 058a18d24..8a17442a7 100644 --- a/util/gvalid/gvalid_validator.go +++ b/util/gvalid/gvalid_validator.go @@ -21,6 +21,7 @@ type Validator struct { 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. } // New creates and returns a new Validator. @@ -55,6 +56,13 @@ func (v *Validator) Bail() *Validator { return newValidator } +// CaseInsensitive sets the mark for Case-Insensitive for those rules that need value comparison. +func (v *Validator) CaseInsensitive() *Validator { + newValidator := v.Clone() + newValidator.caseInsensitive = true + return newValidator +} + // Data is a chaining operation function, which sets validation data for current operation. // The parameter `data` is usually type of map, which specifies the parameter map used in validation. // Calling this function also sets `useDataInsteadOfObjectAttributes` true no mather the `data` is nil or not. diff --git a/util/gvalid/gvalid_validator_check_value.go b/util/gvalid/gvalid_validator_check_value.go index 31b342a5e..3ecbe92d1 100644 --- a/util/gvalid/gvalid_validator_check_value.go +++ b/util/gvalid/gvalid_validator_check_value.go @@ -54,9 +54,9 @@ type doCheckValueInput struct { } // doCheckSingleValue does the really rules validation for single key-value. -func (v *Validator) doCheckValue(ctx context.Context, input doCheckValueInput) Error { +func (v *Validator) doCheckValue(ctx context.Context, in doCheckValueInput) Error { // If there's no validation rules, it does nothing and returns quickly. - if input.Rule == "" { + if in.Rule == "" { return nil } // It converts value to string and then does the validation. @@ -69,17 +69,17 @@ func (v *Validator) doCheckValue(ctx context.Context, input doCheckValueInput) E msgArray = make([]string, 0) customMsgMap = make(map[string]string) ) - switch messages := input.Messages.(type) { + switch messages := in.Messages.(type) { case string: msgArray = strings.Split(messages, "|") default: - for k, message := range gconv.Map(input.Messages) { + for k, message := range gconv.Map(in.Messages) { customMsgMap[k] = gconv.String(message) } } // Handle the char '|' in the rule, // which makes this rule separated into multiple rules. - ruleItems := strings.Split(strings.TrimSpace(input.Rule), "|") + ruleItems := strings.Split(strings.TrimSpace(in.Rule), "|") for i := 0; ; { array := strings.Split(ruleItems[i], ":") _, ok := allSupportedRules[array[0]] @@ -90,7 +90,7 @@ func (v *Validator) doCheckValue(ctx context.Context, input doCheckValueInput) E } else { return newValidationErrorByStr( internalRulesErrRuleName, - errors.New(internalRulesErrRuleName+": "+input.Rule), + errors.New(internalRulesErrRuleName+": "+in.Rule), ) } } else { @@ -101,7 +101,8 @@ func (v *Validator) doCheckValue(ctx context.Context, input doCheckValueInput) E } } var ( - hasBailRule = false + hasBailRule = v.bail + hasCaseInsensitive = v.caseInsensitive ) for index := 0; index < len(ruleItems); { var ( @@ -113,10 +114,14 @@ func (v *Validator) doCheckValue(ctx context.Context, input doCheckValueInput) E customRuleFunc RuleFunc ) - if !hasBailRule && ruleKey == bailRuleName { + if !hasBailRule && ruleKey == ruleNameBail { hasBailRule = true } + if !hasCaseInsensitive && ruleKey == ruleNameCi { + hasCaseInsensitive = true + } + // Ignore logic executing for marked rules. if markedRuleMap[ruleKey] { index++ @@ -138,8 +143,8 @@ func (v *Validator) doCheckValue(ctx context.Context, input doCheckValueInput) E if err = customRuleFunc(ctx, RuleFuncInput{ Rule: ruleItems[index], Message: message, - Value: gvar.New(input.Value), - Data: gvar.New(input.DataRaw), + Value: gvar.New(in.Value), + Data: gvar.New(in.DataRaw), }); err != nil { match = false // The error should have stack info to indicate the error position. @@ -161,13 +166,14 @@ func (v *Validator) doCheckValue(ctx context.Context, input doCheckValueInput) E match, err = v.doCheckSingleBuildInRules( ctx, doCheckBuildInRulesInput{ - Index: index, - Value: input.Value, - RuleKey: ruleKey, - RulePattern: rulePattern, - RuleItems: ruleItems, - DataMap: input.DataMap, - CustomMsgMap: customMsgMap, + Index: index, + Value: in.Value, + RuleKey: ruleKey, + RulePattern: rulePattern, + RuleItems: ruleItems, + DataMap: in.DataMap, + CustomMsgMap: customMsgMap, + CaseInsensitive: hasCaseInsensitive, }, ) if !match && err != nil { @@ -187,9 +193,9 @@ func (v *Validator) doCheckValue(ctx context.Context, input doCheckValueInput) E if err = ruleErrorMap[ruleKey]; !gerror.HasStack(err) { var s string s = gstr.ReplaceByMap(err.Error(), map[string]string{ - "{value}": gconv.String(input.Value), + "{value}": gconv.String(in.Value), "{pattern}": rulePattern, - "{attribute}": input.Name, + "{attribute}": in.Name, }) s, _ = gregex.ReplaceString(`\s{2,}`, ` `, s) ruleErrorMap[ruleKey] = errors.New(s) @@ -206,9 +212,9 @@ func (v *Validator) doCheckValue(ctx context.Context, input doCheckValueInput) E if len(ruleErrorMap) > 0 { return newValidationError( gcode.CodeValidationFailed, - []fieldRule{{Name: input.Name, Rule: input.Rule}}, + []fieldRule{{Name: in.Name, Rule: in.Rule}}, map[string]map[string]error{ - input.Name: ruleErrorMap, + in.Name: ruleErrorMap, }, ) } @@ -216,18 +222,19 @@ func (v *Validator) doCheckValue(ctx context.Context, input doCheckValueInput) E } type doCheckBuildInRulesInput struct { - Index int // Index of RuleKey in RuleItems. - Value interface{} // Value to be validated. - RuleKey string // RuleKey is like the "max" in rule "max: 6" - RulePattern string // RulePattern is like "6" in rule:"max:6" - RuleItems []string // RuleItems are all the rules that should be validated on single field, like: []string{"required", "min:1"} - DataMap map[string]interface{} // Parameter map. - CustomMsgMap map[string]string // Custom error message map. + Index int // Index of RuleKey in RuleItems. + Value interface{} // Value to be validated. + RuleKey string // RuleKey is like the "max" in rule "max: 6" + RulePattern string // RulePattern is like "6" in rule:"max:6" + RuleItems []string // RuleItems are all the rules that should be validated on single field, like: []string{"required", "min:1"} + DataMap map[string]interface{} // Parameter map. + CustomMsgMap map[string]string // Custom error message map. + CaseInsensitive bool // Case-Insensitive comparison. } -func (v *Validator) doCheckSingleBuildInRules(ctx context.Context, input doCheckBuildInRulesInput) (match bool, err error) { - valueStr := gconv.String(input.Value) - switch input.RuleKey { +func (v *Validator) doCheckSingleBuildInRules(ctx context.Context, in doCheckBuildInRulesInput) (match bool, err error) { + valueStr := gconv.String(in.Value) + switch in.RuleKey { // Required rules. case "required", @@ -237,7 +244,13 @@ func (v *Validator) doCheckSingleBuildInRules(ctx context.Context, input doCheck "required-with-all", "required-without", "required-without-all": - match = v.checkRequired(input.Value, input.RuleKey, input.RulePattern, input.DataMap) + match = v.checkRequired(checkRequiredInput{ + Value: in.Value, + RuleKey: in.RuleKey, + RulePattern: in.RulePattern, + DataMap: in.DataMap, + CaseInsensitive: in.CaseInsensitive, + }) // Length rules. // It also supports length of unicode string. @@ -246,7 +259,7 @@ func (v *Validator) doCheckSingleBuildInRules(ctx context.Context, input doCheck "min-length", "max-length", "size": - if msg := v.checkLength(ctx, valueStr, input.RuleKey, input.RulePattern, input.CustomMsgMap); msg != "" { + if msg := v.checkLength(ctx, valueStr, in.RuleKey, in.RulePattern, in.CustomMsgMap); msg != "" { return match, errors.New(msg) } else { match = true @@ -257,7 +270,7 @@ func (v *Validator) doCheckSingleBuildInRules(ctx context.Context, input doCheck "min", "max", "between": - if msg := v.checkRange(ctx, valueStr, input.RuleKey, input.RulePattern, input.CustomMsgMap); msg != "" { + if msg := v.checkRange(ctx, valueStr, in.RuleKey, in.RulePattern, in.CustomMsgMap); msg != "" { return match, errors.New(msg) } else { match = true @@ -266,27 +279,27 @@ func (v *Validator) doCheckSingleBuildInRules(ctx context.Context, input doCheck // Custom regular expression. case "regex": // It here should check the rule as there might be special char '|' in it. - for i := input.Index + 1; i < len(input.RuleItems); i++ { - if !gregex.IsMatchString(singleRulePattern, input.RuleItems[i]) { - input.RulePattern += "|" + input.RuleItems[i] - input.Index++ + for i := in.Index + 1; i < len(in.RuleItems); i++ { + if !gregex.IsMatchString(singleRulePattern, in.RuleItems[i]) { + in.RulePattern += "|" + in.RuleItems[i] + in.Index++ } } - match = gregex.IsMatchString(input.RulePattern, valueStr) + match = gregex.IsMatchString(in.RulePattern, valueStr) // Date rules. case "date": // support for time value, eg: gtime.Time/*gtime.Time, time.Time/*time.Time. - if v, ok := input.Value.(iTime); ok { - return !v.IsZero(), nil + if value, ok := in.Value.(iTime); ok { + return !value.IsZero(), nil } match = gregex.IsMatchString(`\d{4}[\.\-\_/]{0,1}\d{2}[\.\-\_/]{0,1}\d{2}`, valueStr) // Datetime rule. case "datetime": // support for time value, eg: gtime.Time/*gtime.Time, time.Time/*time.Time. - if v, ok := input.Value.(iTime); ok { - return !v.IsZero(), nil + if value, ok := in.Value.(iTime); ok { + return !value.IsZero(), nil } if _, err = gtime.StrToTimeFormat(valueStr, `Y-m-d H:i:s`); err == nil { match = true @@ -295,54 +308,61 @@ func (v *Validator) doCheckSingleBuildInRules(ctx context.Context, input doCheck // Date rule with specified format. case "date-format": // support for time value, eg: gtime.Time/*gtime.Time, time.Time/*time.Time. - if v, ok := input.Value.(iTime); ok { - return !v.IsZero(), nil + if value, ok := in.Value.(iTime); ok { + return !value.IsZero(), nil } - if _, err = gtime.StrToTimeFormat(valueStr, input.RulePattern); err == nil { + if _, err = gtime.StrToTimeFormat(valueStr, in.RulePattern); err == nil { match = true } else { var ( msg string ) - msg = v.getErrorMessageByRule(ctx, input.RuleKey, input.CustomMsgMap) + msg = v.getErrorMessageByRule(ctx, in.RuleKey, in.CustomMsgMap) return match, errors.New(msg) } // Values of two fields should be equal as string. case "same": - _, foundValue := gutil.MapPossibleItemByKey(input.DataMap, input.RulePattern) + _, foundValue := gutil.MapPossibleItemByKey(in.DataMap, in.RulePattern) if foundValue != nil { - if strings.Compare(valueStr, gconv.String(foundValue)) == 0 { - match = true + if in.CaseInsensitive { + match = strings.EqualFold(valueStr, gconv.String(foundValue)) + } else { + match = strings.Compare(valueStr, gconv.String(foundValue)) == 0 } } if !match { var msg string - msg = v.getErrorMessageByRule(ctx, input.RuleKey, input.CustomMsgMap) + msg = v.getErrorMessageByRule(ctx, in.RuleKey, in.CustomMsgMap) return match, errors.New(msg) } // Values of two fields should not be equal as string. case "different": match = true - _, foundValue := gutil.MapPossibleItemByKey(input.DataMap, input.RulePattern) + _, foundValue := gutil.MapPossibleItemByKey(in.DataMap, in.RulePattern) if foundValue != nil { - if strings.Compare(valueStr, gconv.String(foundValue)) == 0 { - match = false + if in.CaseInsensitive { + match = !strings.EqualFold(valueStr, gconv.String(foundValue)) + } else { + match = strings.Compare(valueStr, gconv.String(foundValue)) != 0 } } if !match { var msg string - msg = v.getErrorMessageByRule(ctx, input.RuleKey, input.CustomMsgMap) + msg = v.getErrorMessageByRule(ctx, in.RuleKey, in.CustomMsgMap) return match, errors.New(msg) } // Field value should be in range of. case "in": - array := gstr.SplitAndTrim(input.RulePattern, ",") - for _, v := range array { - if strings.Compare(valueStr, strings.TrimSpace(v)) == 0 { - match = true + for _, value := range gstr.SplitAndTrim(in.RulePattern, ",") { + if in.CaseInsensitive { + match = strings.EqualFold(valueStr, strings.TrimSpace(value)) + } else { + match = strings.Compare(valueStr, strings.TrimSpace(value)) == 0 + } + if match { break } } @@ -350,10 +370,13 @@ func (v *Validator) doCheckSingleBuildInRules(ctx context.Context, input doCheck // Field value should not be in range of. case "not-in": match = true - array := gstr.SplitAndTrim(input.RulePattern, ",") - for _, v := range array { - if strings.Compare(valueStr, strings.TrimSpace(v)) == 0 { - match = false + for _, value := range gstr.SplitAndTrim(in.RulePattern, ",") { + if in.CaseInsensitive { + match = !strings.EqualFold(valueStr, strings.TrimSpace(value)) + } else { + match = strings.Compare(valueStr, strings.TrimSpace(value)) != 0 + } + if !match { break } } @@ -517,7 +540,7 @@ func (v *Validator) doCheckSingleBuildInRules(ctx context.Context, input doCheck match = gregex.IsMatchString(`^([0-9A-Fa-f]{2}[\-:]){5}[0-9A-Fa-f]{2}$`, valueStr) default: - return match, errors.New("Invalid rule name: " + input.RuleKey) + return match, errors.New("Invalid rule name: " + in.RuleKey) } return match, nil } diff --git a/util/gvalid/gvalid_validator_rule_required.go b/util/gvalid/gvalid_validator_rule_required.go index 06de0c50d..c09dedf24 100644 --- a/util/gvalid/gvalid_validator_rule_required.go +++ b/util/gvalid/gvalid_validator_rule_required.go @@ -15,11 +15,19 @@ import ( "github.com/gogf/gf/v2/util/gutil" ) +type checkRequiredInput struct { + Value interface{} // Value to be validated. + RuleKey string // RuleKey is like the "max" in rule "max: 6" + RulePattern string // RulePattern is like "6" in rule:"max:6" + DataMap map[string]interface{} // Parameter map. + CaseInsensitive bool // Case-Insensitive comparison. +} + // checkRequired checks `value` using required rules. // It also supports require checks for `value` of type: slice, map. -func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string, dataMap map[string]interface{}) bool { +func (v *Validator) checkRequired(in checkRequiredInput) bool { required := false - switch ruleKey { + switch in.RuleKey { // Required. case "required": required = true @@ -29,7 +37,7 @@ func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string case "required-if": required = false var ( - array = strings.Split(rulePattern, ",") + array = strings.Split(in.RulePattern, ",") foundValue interface{} ) // It supports multiple field and value pairs. @@ -37,9 +45,13 @@ func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string for i := 0; i < len(array); { tk := array[i] tv := array[i+1] - _, foundValue = gutil.MapPossibleItemByKey(dataMap, tk) - if strings.Compare(tv, gconv.String(foundValue)) == 0 { - required = true + _, foundValue = gutil.MapPossibleItemByKey(in.DataMap, tk) + if in.CaseInsensitive { + required = strings.EqualFold(tv, gconv.String(foundValue)) + } else { + required = strings.Compare(tv, gconv.String(foundValue)) == 0 + } + if required { break } i += 2 @@ -51,7 +63,7 @@ func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string case "required-unless": required = true var ( - array = strings.Split(rulePattern, ",") + array = strings.Split(in.RulePattern, ",") foundValue interface{} ) // It supports multiple field and value pairs. @@ -59,12 +71,15 @@ func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string for i := 0; i < len(array); { tk := array[i] tv := array[i+1] - _, foundValue = gutil.MapPossibleItemByKey(dataMap, tk) - if strings.Compare(tv, gconv.String(foundValue)) == 0 { - required = false + _, foundValue = gutil.MapPossibleItemByKey(in.DataMap, tk) + if in.CaseInsensitive { + required = !strings.EqualFold(tv, gconv.String(foundValue)) + } else { + required = strings.Compare(tv, gconv.String(foundValue)) != 0 + } + if !required { break } - i += 2 } } @@ -74,11 +89,11 @@ func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string case "required-with": required = false var ( - array = strings.Split(rulePattern, ",") + array = strings.Split(in.RulePattern, ",") foundValue interface{} ) for i := 0; i < len(array); i++ { - _, foundValue = gutil.MapPossibleItemByKey(dataMap, array[i]) + _, foundValue = gutil.MapPossibleItemByKey(in.DataMap, array[i]) if !empty.IsEmpty(foundValue) { required = true break @@ -90,11 +105,11 @@ func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string case "required-with-all": required = true var ( - array = strings.Split(rulePattern, ",") + array = strings.Split(in.RulePattern, ",") foundValue interface{} ) for i := 0; i < len(array); i++ { - _, foundValue = gutil.MapPossibleItemByKey(dataMap, array[i]) + _, foundValue = gutil.MapPossibleItemByKey(in.DataMap, array[i]) if empty.IsEmpty(foundValue) { required = false break @@ -106,11 +121,11 @@ func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string case "required-without": required = false var ( - array = strings.Split(rulePattern, ",") + array = strings.Split(in.RulePattern, ",") foundValue interface{} ) for i := 0; i < len(array); i++ { - _, foundValue = gutil.MapPossibleItemByKey(dataMap, array[i]) + _, foundValue = gutil.MapPossibleItemByKey(in.DataMap, array[i]) if empty.IsEmpty(foundValue) { required = true break @@ -122,11 +137,11 @@ func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string case "required-without-all": required = true var ( - array = strings.Split(rulePattern, ",") + array = strings.Split(in.RulePattern, ",") foundValue interface{} ) for i := 0; i < len(array); i++ { - _, foundValue = gutil.MapPossibleItemByKey(dataMap, array[i]) + _, foundValue = gutil.MapPossibleItemByKey(in.DataMap, array[i]) if !empty.IsEmpty(foundValue) { required = false break @@ -134,7 +149,7 @@ func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string } } if required { - reflectValue := reflect.ValueOf(value) + reflectValue := reflect.ValueOf(in.Value) for reflectValue.Kind() == reflect.Ptr { reflectValue = reflectValue.Elem() } @@ -142,7 +157,7 @@ func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string case reflect.String, reflect.Map, reflect.Array, reflect.Slice: return reflectValue.Len() != 0 } - return gconv.String(value) != "" + return gconv.String(in.Value) != "" } else { return true } diff --git a/util/gvalid/gvalid_z_unit_feature_checkmap_test.go b/util/gvalid/gvalid_z_unit_feature_checkmap_test.go index 4f0849a76..d7c77cbc8 100755 --- a/util/gvalid/gvalid_z_unit_feature_checkmap_test.go +++ b/util/gvalid/gvalid_z_unit_feature_checkmap_test.go @@ -226,7 +226,7 @@ func Test_Map_Bail(t *testing.T) { } err := g.Validator().Bail().Rules(rules).CheckMap(ctx, params) t.AssertNE(err, nil) - t.Assert(err.String(), "账号不能为空; 账号长度应当在6到16之间") + t.Assert(err.String(), "账号不能为空") }) // global bail with rule bail gtest.C(t, func(t *gtest.T) { diff --git a/util/gvalid/gvalid_z_unit_feature_ci_test.go b/util/gvalid/gvalid_z_unit_feature_ci_test.go new file mode 100644 index 000000000..a2600110c --- /dev/null +++ b/util/gvalid/gvalid_z_unit_feature_ci_test.go @@ -0,0 +1,30 @@ +// 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 ( + "testing" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/test/gtest" + "github.com/gogf/gf/v2/util/gvalid" +) + +func Test_CI(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + err := gvalid.CheckValue(ctx, "id", "in:Id,Name", nil) + t.AssertNE(err, nil) + }) + gtest.C(t, func(t *gtest.T) { + err := gvalid.CheckValue(ctx, "id", "ci|in:Id,Name", nil) + t.AssertNil(err) + }) + gtest.C(t, func(t *gtest.T) { + err := g.Validator().CaseInsensitive().Rules("in:Id,Name").CheckValue(ctx, "id") + t.AssertNil(err) + }) +}