Merge branch 'master' into feature/recursive-validation

This commit is contained in:
John Guo
2021-11-22 14:45:53 +08:00
28 changed files with 1550 additions and 298 deletions

View File

@ -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`.

View File

@ -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)

View File

@ -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.

68
os/gcmd/gcmd_command.go Normal file
View File

@ -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
}

View File

@ -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
}

107
os/gcmd/gcmd_command_run.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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, "")
}

View File

@ -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)
})
}

View File

@ -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()

View File

@ -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
}

View File

@ -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]
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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]
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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]
}

View File

@ -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
}

View File

@ -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)

View File

@ -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)

View File

@ -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,
}
)

View File

@ -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.

View File

@ -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
}

View File

@ -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
}

View File

@ -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) {

View File

@ -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)
})
}