├── go.mod ├── .github ├── dependabot.yml ├── ISSUE_TEMPLATE │ └── bug_report.md ├── changelog.yml └── workflows │ ├── release.yml │ ├── go.yml │ └── codeql.yml ├── filter_test.go ├── go.sum ├── LICENSE ├── filters_test.go ├── filter.go ├── README.zh-CN.md ├── README.md ├── filters.go ├── converters.go ├── filtration_test.go ├── converters_test.go └── filtration.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gookit/filter 2 | 3 | go 1.19 4 | 5 | require github.com/gookit/goutil v0.7.2 6 | 7 | require ( 8 | golang.org/x/sys v0.30.0 // indirect 9 | golang.org/x/term v0.29.0 // indirect 10 | golang.org/x/text v0.22.0 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | # Check for updates to GitHub Actions every weekday 13 | interval: "daily" 14 | 15 | -------------------------------------------------------------------------------- /filter_test.go: -------------------------------------------------------------------------------- 1 | package filter_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gookit/filter" 7 | "github.com/gookit/goutil/testutil/assert" 8 | ) 9 | 10 | func TestApply(t *testing.T) { 11 | str := " abc " 12 | ret, err := filter.Apply("trim", str, nil) 13 | assert.NoErr(t, err) 14 | assert.Equal(t, "abc", ret) 15 | 16 | // test pointer string 17 | ps := &str 18 | ret, err = filter.Apply("trim", ps, nil) 19 | assert.NoErr(t, err) 20 | assert.Equal(t, "abc", ret) 21 | } 22 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/gookit/goutil v0.7.2 h1:NSiqWWY+BT0MwIlKDeSVPfQmr9xTkkAqwDjhplobdgo= 2 | github.com/gookit/goutil v0.7.2/go.mod h1:vJS9HXctYTCLtCsZot5L5xF+O1oR17cDYO9R0HxBmnU= 3 | golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= 4 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 5 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 6 | golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= 7 | golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= 8 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 9 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: inhere 7 | 8 | --- 9 | 10 | **System (please complete the following information):** 11 | 12 | - OS: `linux` [e.g. linux, macOS] 13 | - GO Version: `1.13` [e.g. `1.13`] 14 | - Pkg Version: `1.1.1` [e.g. `1.1.1`] 15 | 16 | **Describe the bug** 17 | 18 | A clear and concise description of what the bug is. 19 | 20 | **To Reproduce** 21 | 22 | ```go 23 | // go code 24 | ``` 25 | 26 | **Expected behavior** 27 | 28 | A clear and concise description of what you expected to happen. 29 | 30 | **Screenshots** 31 | 32 | If applicable, add screenshots to help explain your problem. 33 | 34 | **Additional context** 35 | 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /.github/changelog.yml: -------------------------------------------------------------------------------- 1 | title: '## Change Log' 2 | # style allow: simple, markdown(mkdown), ghr(gh-release) 3 | style: gh-release 4 | # group names 5 | names: [Refactor, Fixed, Feature, Update, Other] 6 | # if empty will auto fetch by git remote 7 | #repo_url: https://github.com/gookit/gitw 8 | 9 | filters: 10 | # message length should >= 12 11 | - name: msg_len 12 | min_len: 12 13 | # message words should >= 3 14 | - name: words_len 15 | min_len: 3 16 | - name: keyword 17 | keyword: format code 18 | exclude: true 19 | - name: keywords 20 | keywords: format code, action test 21 | exclude: true 22 | 23 | # group match rules 24 | # not matched will use 'Other' group. 25 | rules: 26 | - name: Refactor 27 | start_withs: [refactor, break] 28 | contains: ['refactor:'] 29 | - name: Fixed 30 | start_withs: [fix] 31 | contains: ['fix:'] 32 | - name: Feature 33 | start_withs: [feat, new] 34 | contains: ['feat:'] 35 | - name: Update 36 | start_withs: [up] 37 | contains: ['update:', 'up:'] 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 inhere 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Tag-release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | release: 10 | name: Release new version 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 10 13 | strategy: 14 | fail-fast: true 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v6 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Setup ENV 23 | # https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-an-environment-variable 24 | run: | 25 | echo "RELEASE_TAG=${GITHUB_REF:10}" >> $GITHUB_ENV 26 | echo "RELEASE_NAME=$GITHUB_WORKFLOW" >> $GITHUB_ENV 27 | 28 | - name: Generate changelog 29 | run: | 30 | curl https://github.com/gookit/gitw/releases/latest/download/chlog-linux-amd64 -L -o /usr/local/bin/chlog 31 | chmod a+x /usr/local/bin/chlog 32 | chlog -c .github/changelog.yml -o changelog.md prev last 33 | 34 | # https://github.com/softprops/action-gh-release 35 | - name: Create release and upload assets 36 | uses: softprops/action-gh-release@v2 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | with: 40 | name: ${{ env.RELEASE_TAG }} 41 | tag_name: ${{ env.RELEASE_TAG }} 42 | body_path: changelog.md 43 | token: ${{ secrets.GITHUB_TOKEN }} 44 | # files: macos-chlog.exe 45 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Unit-Tests 2 | on: 3 | pull_request: 4 | paths: 5 | - 'go.mod' 6 | - '**.go' 7 | - '**.yml' 8 | push: 9 | paths: 10 | - '**.go' 11 | - 'go.mod' 12 | - '**.yml' 13 | 14 | # https://github.com/actions 15 | jobs: 16 | 17 | test: 18 | name: Test on go ${{ matrix.go_version }} 19 | runs-on: ubuntu-latest 20 | strategy: 21 | matrix: 22 | go_version: [1.19, '1.20', 1.21] 23 | 24 | steps: 25 | - name: Check out code 26 | uses: actions/checkout@v6 27 | with: 28 | fetch-depth: 0 29 | 30 | - name: Display Env 31 | run: | 32 | git remote -v 33 | git tag -l 34 | env 35 | 36 | - name: Setup Go Faster 37 | uses: WillAbides/setup-go-faster@v1.14.0 38 | timeout-minutes: 3 39 | with: 40 | go-version: ${{ matrix.go_version }} 41 | 42 | - name: Revive check 43 | uses: docker://morphy/revive-action:v2.3.1 44 | with: 45 | # Exclude patterns, separated by semicolons (optional) 46 | exclude: "./internal/..." 47 | 48 | - name: Run static check 49 | uses: reviewdog/action-staticcheck@v1 50 | if: ${{ github.event_name == 'pull_request'}} 51 | with: 52 | github_token: ${{ secrets.github_token }} 53 | # Change reviewdog reporter if you need [github-pr-check,github-check,github-pr-review]. 54 | reporter: github-pr-check 55 | # Report all results. [added,diff_context,file,nofilter]. 56 | filter_mode: added 57 | # Exit with 1 when it find at least one finding. 58 | fail_on_error: true 59 | 60 | - name: Run tests 61 | run: | 62 | pwd 63 | go test -v -cover ./... 64 | # go run ./cmd/chlog last head 65 | -------------------------------------------------------------------------------- /filters_test.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/gookit/goutil/testutil/assert" 8 | ) 9 | 10 | func TestTrim(t *testing.T) { 11 | is := assert.New(t) 12 | 13 | // Trim 14 | tests := map[string]string{ 15 | "abc ": "", 16 | " abc": "", 17 | " abc ": "", 18 | "abc,,": ",", 19 | "abc,.": ",.", 20 | } 21 | for sample, cutSet := range tests { 22 | is.Eq("abc", Trim(sample, cutSet)) 23 | } 24 | 25 | is.Eq("abc", Trim("abc,.", ".,")) 26 | // is.Eq("", Trim(nil)) 27 | 28 | // TrimLeft 29 | is.Eq("abc ", TrimLeft(" abc ")) 30 | is.Eq("abc ,", TrimLeft(", abc ,", " ,")) 31 | is.Eq("abc ,", TrimLeft(", abc ,", ", ")) 32 | 33 | // TrimRight 34 | is.Eq(" abc", TrimRight(" abc ")) 35 | is.Eq(", abc", TrimRight(", abc ,", ", ")) 36 | 37 | // TrimStrings 38 | ss := TrimStrings([]string{" a", "b ", " c "}) 39 | is.Eq("[a b c]", fmt.Sprint(ss)) 40 | ss = TrimStrings([]string{",a", "b.", ",.c,"}, ",.") 41 | is.Eq("[a b c]", fmt.Sprint(ss)) 42 | } 43 | 44 | func TestEmail(t *testing.T) { 45 | is := assert.New(t) 46 | is.Eq("THE@inhere.com", Email(" THE@INHere.com ")) 47 | is.Eq("inhere.xyz", Email(" inhere.xyz ")) 48 | } 49 | 50 | func TestStrOperate(t *testing.T) { 51 | is := assert.New(t) 52 | 53 | // Substr 54 | is.Eq("DEF", Substr("abcDEF", 3, 3)) 55 | is.Eq("DEF", Substr("abcDEF", 3, 5)) 56 | is.Eq("", Substr("abcDEF", 23, 5)) 57 | } 58 | 59 | func TestURLEnDecode(t *testing.T) { 60 | is := assert.New(t) 61 | 62 | is.Eq("a.com/?name%3D%E4%BD%A0%E5%A5%BD", URLEncode("a.com/?name=你好")) 63 | is.Eq("a.com/?name=你好", URLDecode("a.com/?name%3D%E4%BD%A0%E5%A5%BD")) 64 | is.Eq("a.com", URLEncode("a.com")) 65 | is.Eq("a.com", URLDecode("a.com")) 66 | } 67 | 68 | func TestUnique(t *testing.T) { 69 | is := assert.New(t) 70 | 71 | is.Len(Unique([]int{1, 2}), 2) 72 | is.Len(Unique([]int{1, 2, 2, 1}), 2) 73 | is.Len(Unique([]int64{1, 2}), 2) 74 | is.Len(Unique([]int64{1, 2, 2, 1}), 2) 75 | is.Len(Unique([]string{"a", "b"}), 2) 76 | is.Len(Unique([]string{"a", "b", "b"}), 2) 77 | is.Eq("invalid", Unique("invalid")) 78 | } 79 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master" ] 20 | schedule: 21 | - cron: '28 10 * * 0' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v6 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v4 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v4 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v4 73 | -------------------------------------------------------------------------------- /filter.go: -------------------------------------------------------------------------------- 1 | // Package filter provide data filter, sanitize, convert process. 2 | // 3 | // Source code and other details for the project are available at GitHub: 4 | // 5 | // https://github.com/gookit/filter 6 | // 7 | // More usage please see README and tests 8 | package filter 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/gookit/goutil/arrutil" 14 | "github.com/gookit/goutil/maputil" 15 | "github.com/gookit/goutil/mathutil" 16 | "github.com/gookit/goutil/strutil" 17 | ) 18 | 19 | // Apply a filter by name. for filter value. 20 | func Apply(name string, val any, args []string) (any, error) { 21 | var err error 22 | realName := Name(name) 23 | 24 | // don't limit value type 25 | if _, ok := dontLimitType[realName]; ok { 26 | switch realName { 27 | case "int": 28 | val, err = mathutil.ToInt(val) 29 | case "uint": 30 | val, err = mathutil.ToUint(val) 31 | case "int64": 32 | val, err = mathutil.ToInt64(val) 33 | case "float": 34 | val, err = mathutil.ToFloat(val) 35 | case "unique": 36 | val = Unique(val) 37 | case "trimStrings": 38 | if ss, ok := val.([]string); ok { 39 | val = arrutil.TrimStrings(ss) 40 | } else { 41 | err = errInvalidParam 42 | } 43 | case "stringsToInts": 44 | if ss, ok := val.([]string); ok { 45 | val, err = arrutil.StringsToInts(ss) 46 | } else { 47 | err = errInvalidParam 48 | } 49 | } 50 | return val, err 51 | } 52 | 53 | // check val is string 54 | var str string 55 | 56 | // up: support filter pointer string value 57 | if poStr, ok := val.(*string); ok { 58 | str = *poStr 59 | } else if str, ok = val.(string); !ok { 60 | return nil, fmt.Errorf("filter: '%s' only use for string type, input %T", name, val) 61 | } 62 | 63 | // val is must be string. 64 | switch realName { 65 | case "bool": 66 | val, err = strutil.ToBool(str) 67 | case "trim": 68 | val = strutil.Trim(str, args...) 69 | case "trimLeft": 70 | val = strutil.TrimLeft(str, args...) 71 | case "trimRight": 72 | val = strutil.TrimRight(str, args...) 73 | case "title": 74 | val = Title(str) 75 | case "email": 76 | val = strutil.FilterEmail(str) 77 | case "substr": 78 | val = strutil.Substr(str, MustInt(args[0]), MustInt(args[1])) 79 | case "lower": 80 | val = strutil.Lowercase(str) 81 | case "upper": 82 | val = strutil.Uppercase(str) 83 | case "lowerFirst": 84 | val = strutil.LowerFirst(str) 85 | case "upperFirst": 86 | val = strutil.UpperFirst(str) 87 | case "upperWord": 88 | val = strutil.UpperWord(str) 89 | case "snakeCase": 90 | val = strutil.SnakeCase(str, args...) 91 | case "camelCase": 92 | val = strutil.CamelCase(str, args...) 93 | case "URLEncode": 94 | val = strutil.URLEncode(str) 95 | case "URLDecode": 96 | val = strutil.URLDecode(str) 97 | case "escapeJS": 98 | val = strutil.EscapeJS(str) 99 | case "escapeHTML": 100 | val = strutil.EscapeHTML(str) 101 | case "strToInts": 102 | val, err = strutil.ToInts(str, args...) 103 | case "strToSlice": 104 | val = strutil.ToSlice(str, args...) 105 | case "strToTime": 106 | val, err = strutil.ToTime(str, args...) 107 | } 108 | 109 | return val, err 110 | } 111 | 112 | // GetByPath get value from a map[string]any. eg "top" "top.sub" 113 | func GetByPath(key string, mp map[string]any) (any, bool) { 114 | return maputil.GetByPath(key, mp) 115 | } 116 | 117 | func parseArgString(argStr string) (ss []string) { 118 | if argStr == "" { // no arg 119 | return 120 | } 121 | 122 | if len(argStr) == 1 { // one char 123 | return []string{argStr} 124 | } 125 | return strutil.Split(argStr, ",") 126 | } 127 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | # Filter 2 | 3 | [](https://github.com/gookit/filter) 4 | [](https://github.com/gookit/filter/actions) 5 | [](https://coveralls.io/github/gookit/filter?branch=master) 6 | [](https://goreportcard.com/report/github.com/gookit/filter) 7 | [](https://pkg.go.dev/github.com/gookit/filter) 8 | 9 | `filter` - 提供过滤,净化以及转换Go数据等. 10 | 11 | > English, please see **[README](README.md)** 12 | 13 | ## GoDoc 14 | 15 | - [godoc](https://pkg.go.dev/github.com/gookit/filter) 16 | 17 | > TIP: 想要过滤并验证Map,Struct数据,请使用 A [gookit/validate](https://github.com/gookit/validate) 18 | 19 | ## 安装 20 | 21 | ```shell 22 | go get github.com/gookit/filter 23 | ``` 24 | 25 | ## 直接使用方法 26 | 27 | Quick usage: 28 | 29 | ```go 30 | str := filter.MustString(23) // "23" 31 | 32 | intVal, err := filter.Int("20") // int(20) 33 | strings := filter.Str2Slice("a,b, c", ",") // []string{"a", "b", "c"} 34 | ``` 35 | 36 | ## 过滤Map 37 | 38 | Filtering data: 39 | 40 | ```go 41 | data := map[string]any{ 42 | "name": " inhere ", 43 | "age": "50", 44 | "money": "50.34", 45 | // 46 | "remember": "yes", 47 | // 48 | "sub1": []string{"1", "2"}, 49 | "tags": "go;lib", 50 | "str1": " word ", 51 | "ids": []int{1, 2, 2, 1}, 52 | } 53 | f := filter.New(data) 54 | f.AddRule("money", "float") 55 | f.AddRule("remember", "bool") 56 | f.AddRule("sub1", "strings2ints") 57 | f.AddRule("tags", "str2arr:;") 58 | f.AddRule("ids", "unique") 59 | f.AddRule("str1", "ltrim|rtrim") 60 | f.AddRule("not-exist", "unique") 61 | // add multi 62 | f.AddRules(map[string]string{ 63 | "age": "trim|int", 64 | "name": "trim|ucFirst", 65 | }) 66 | 67 | // apply all added rules for data. 68 | f.Filtering() 69 | 70 | // get filtered data 71 | newData := f.CleanData() 72 | fmt.Printf("%#v\n", newData) 73 | // f.BindStruct(&user) 74 | ``` 75 | 76 | **Output**: 77 | 78 | ```go 79 | map[string]interface {}{ 80 | "remember":true, 81 | "sub1":[]int{1, 2}, 82 | "tags":[]string{"go", "lib"}, 83 | "ids":[]int{2, 1}, 84 | "str1":"word", 85 | "name":"INHERE", 86 | "age":50, 87 | "money":50.34 88 | } 89 | ``` 90 | 91 | ## Filters & Converters 92 | 93 | - `ToBool/Bool(s string) (bool, error)` 94 | - `ToFloat/Float(v interface{}) (float64, error)` 95 | - `ToInt/Int(v interface{}) (int, error)` 96 | - `ToUint/Uint(v interface{}) (uint64, error)` 97 | - `ToInt64/Int64(v interface{}) (int64, error)` 98 | - `ToString/String(v interface{}) (string, error)` 99 | - `MustBool(s string) bool` 100 | - `MustFloat(s string) float64` 101 | - `MustInt(s string) int` 102 | - `MustInt64(s string) int64` 103 | - `MustUint(s string) uint64` 104 | - `MustString(v interface{}) string` 105 | - `Trim(s string, cutSet ...string) string` 106 | - `TrimLeft(s string, cutSet ...string) string` 107 | - `TrimRight(s string, cutSet ...string) string` 108 | - `TrimStrings(ss []string, cutSet ...string) (ns []string)` 109 | - `Substr(s string, pos, length int) string` 110 | - `Lower/Lowercase(s string) string` 111 | - `Upper/Uppercase(s string) string` 112 | - `LowerFirst(s string) string` 113 | - `UpperFirst(s string) string` 114 | - `UpperWord(s string) string` 115 | - `Camel/CamelCase(s string, sep ...string) string` 116 | - `Snake/SnakeCase(s string, sep ...string) string` 117 | - `Email(s string) string` 118 | - `URLDecode(s string) string` 119 | - `URLEncode(s string) string` 120 | - `EscapeJS(s string) string` 121 | - `EscapeHTML(s string) string` 122 | - `Unique(val interface{}) interface{}` Will remove duplicate values, use for `[]int` `[]int64` `[]string` 123 | - `StrToSlice(s string, sep ...string) []string` 124 | - `StrToInts(s string, sep ...string) (ints []int, err error)` 125 | - `StrToTime(s string, layouts ...string) (t time.Time, err error)` 126 | - `StringsToInts(ss []string) (ints []int, err error)` 127 | 128 | ## License 129 | 130 | **[MIT](LICENSE)** 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Filter 2 | 3 | [](https://github.com/gookit/filter) 4 | [](https://github.com/gookit/filter/actions) 5 | [](https://coveralls.io/github/gookit/filter?branch=master) 6 | [](https://goreportcard.com/report/github.com/gookit/filter) 7 | [](https://pkg.go.dev/github.com/gookit/filter) 8 | 9 | `filter` - provide filtering, sanitizing, and conversion of Golang data. 10 | 11 | > 中文说明请查看 **[README.zh-CN](README.zh-CN.md)** 12 | 13 | ## GoDoc 14 | 15 | - [godoc](https://pkg.go.dev/github.com/gookit/filter) 16 | 17 | > **NOTE**: To filter and validate Map, Struct data. Please use [gookit/validate](https://github.com/gookit/validate) 18 | 19 | ## Install 20 | 21 | ```shell 22 | go get github.com/gookit/filter 23 | ``` 24 | 25 | ## Func Usage 26 | 27 | Quick usage: 28 | 29 | ```go 30 | str := filter.MustString(23) // "23" 31 | 32 | intVal, err := filter.Int("20") // int(20) 33 | strings := filter.Str2Slice("a,b, c", ",") // []string{"a", "b", "c"} 34 | ``` 35 | 36 | ## Filtration 37 | 38 | Filtering data: 39 | 40 | ```go 41 | data := map[string]any{ 42 | "name": " inhere ", 43 | "age": "50", 44 | "money": "50.34", 45 | // 46 | "remember": "yes", 47 | // 48 | "sub1": []string{"1", "2"}, 49 | "tags": "go;lib", 50 | "str1": " word ", 51 | "ids": []int{1, 2, 2, 1}, 52 | } 53 | f := filter.New(data) 54 | f.AddRule("money", "float") 55 | f.AddRule("remember", "bool") 56 | f.AddRule("sub1", "strings2ints") 57 | f.AddRule("tags", "str2arr:;") 58 | f.AddRule("ids", "unique") 59 | f.AddRule("str1", "ltrim|rtrim") 60 | f.AddRule("not-exist", "unique") 61 | // add multi 62 | f.AddRules(map[string]string{ 63 | "age": "trim|int", 64 | "name": "trim|ucFirst", 65 | }) 66 | 67 | // apply all added rules for data. 68 | f.Filtering() 69 | 70 | // get filtered data 71 | newData := f.CleanData() 72 | fmt.Printf("%#v\n", newData) 73 | // f.BindStruct(&user) 74 | ``` 75 | 76 | **Output**: 77 | 78 | ```go 79 | map[string]interface {}{ 80 | "remember":true, 81 | "sub1":[]int{1, 2}, 82 | "tags":[]string{"go", "lib"}, 83 | "ids":[]int{2, 1}, 84 | "str1":"word", 85 | "name":"INHERE", 86 | "age":50, 87 | "money":50.34 88 | } 89 | ``` 90 | 91 | ## Filters & Converters 92 | 93 | - `ToBool/Bool(s string) (bool, error)` 94 | - `ToFloat/Float(v interface{}) (float64, error)` 95 | - `ToInt/Int(v interface{}) (int, error)` 96 | - `ToUint/Uint(v interface{}) (uint64, error)` 97 | - `ToInt64/Int64(v interface{}) (int64, error)` 98 | - `ToString/String(v interface{}) (string, error)` 99 | - `MustBool(s string) bool` 100 | - `MustFloat(s string) float64` 101 | - `MustInt(s string) int` 102 | - `MustInt64(s string) int64` 103 | - `MustUint(s string) uint64` 104 | - `MustString(v interface{}) string` 105 | - `Trim(s string, cutSet ...string) string` 106 | - `TrimLeft(s string, cutSet ...string) string` 107 | - `TrimRight(s string, cutSet ...string) string` 108 | - `TrimStrings(ss []string, cutSet ...string) (ns []string)` 109 | - `Substr(s string, pos, length int) string` 110 | - `Lower/Lowercase(s string) string` 111 | - `Upper/Uppercase(s string) string` 112 | - `LowerFirst(s string) string` 113 | - `UpperFirst(s string) string` 114 | - `UpperWord(s string) string` 115 | - `Camel/CamelCase(s string, sep ...string) string` 116 | - `Snake/SnakeCase(s string, sep ...string) string` 117 | - `Email(s string) string` 118 | - `URLDecode(s string) string` 119 | - `URLEncode(s string) string` 120 | - `EscapeJS(s string) string` 121 | - `EscapeHTML(s string) string` 122 | - `Unique(val interface{}) interface{}` Will remove duplicate values, use for `[]int` `[]int64` `[]string` 123 | - `StrToSlice(s string, sep ...string) []string` 124 | - `StrToInts(s string, sep ...string) (ints []int, err error)` 125 | - `StrToTime(s string, layouts ...string) (t time.Time, err error)` 126 | - `StringsToInts(ss []string) (ints []int, err error)` 127 | 128 | ## License 129 | 130 | **[MIT](LICENSE)** 131 | -------------------------------------------------------------------------------- /filters.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | 7 | "github.com/gookit/goutil/arrutil" 8 | "github.com/gookit/goutil/strutil" 9 | ) 10 | 11 | var dontLimitType = map[string]uint8{ 12 | "int": 1, 13 | "uint": 1, 14 | "int64": 1, 15 | "float": 1, 16 | "unique": 1, 17 | // list 18 | "trimStrings": 1, 19 | "stringsToInts": 1, 20 | } 21 | 22 | var filterAliases = map[string]string{ 23 | "toInt": "int", 24 | "toUint": "uint", 25 | "toInt64": "int64", 26 | "toBool": "bool", 27 | "camel": "camelCase", 28 | "snake": "snakeCase", 29 | "ltrim": "trimLeft", 30 | "rtrim": "trimRight", 31 | // -- 32 | "lcFirst": "lowerFirst", 33 | "ucFirst": "upperFirst", 34 | "ucWord": "upperWord", 35 | "distinct": "unique", 36 | "trimList": "trimStrings", 37 | "trim_list": "trimStrings", 38 | "trim_strings": "trimStrings", 39 | "trimSpace": "trim", 40 | "trim_space": "trim", 41 | "uppercase": "upper", 42 | "lowercase": "lower", 43 | "escapeJs": "escapeJS", 44 | "escape_js": "escapeJS", 45 | "escapeHtml": "escapeHTML", 46 | "escape_html": "escapeHTML", 47 | "urlEncode": "URLEncode", 48 | "url_encode": "URLEncode", 49 | "encodeUrl": "URLEncode", 50 | "encode_url": "URLEncode", 51 | "urlDecode": "URLDecode", 52 | "url_decode": "URLDecode", 53 | "decodeUrl": "URLDecode", 54 | "decode_url": "URLDecode", 55 | // convert string 56 | "str2ints": "strToInts", 57 | "str2arr": "strToSlice", 58 | "str2list": "strToSlice", 59 | "str2array": "strToSlice", 60 | "strToArr": "strToSlice", 61 | "str2time": "strToTime", 62 | // strings to ints 63 | "strings2ints": "stringsToInts", 64 | } 65 | 66 | // Name get real filter name. 67 | func Name(name string) string { 68 | if rName, ok := filterAliases[name]; ok { 69 | return rName 70 | } 71 | return name 72 | } 73 | 74 | /************************************************************* 75 | * built in filters 76 | *************************************************************/ 77 | 78 | // Trim string 79 | func Trim(s string, cutSet ...string) string { 80 | if len(cutSet) > 0 && cutSet[0] != "" { 81 | return strings.Trim(s, cutSet[0]) 82 | } 83 | return strings.TrimSpace(s) 84 | } 85 | 86 | // TrimLeft char in the string. 87 | func TrimLeft(s string, cutSet ...string) string { 88 | if len(cutSet) > 0 { 89 | return strings.TrimLeft(s, cutSet[0]) 90 | } 91 | 92 | return strings.TrimLeft(s, " ") 93 | } 94 | 95 | // TrimRight char in the string. 96 | func TrimRight(s string, cutSet ...string) string { 97 | if len(cutSet) > 0 { 98 | return strings.TrimRight(s, cutSet[0]) 99 | } 100 | return strings.TrimRight(s, " ") 101 | } 102 | 103 | // TrimStrings trim string slice item. 104 | func TrimStrings(ss []string, cutSet ...string) []string { 105 | return arrutil.TrimStrings(ss, cutSet...) 106 | } 107 | 108 | // URLEncode encode url string. 109 | func URLEncode(s string) string { 110 | if pos := strings.IndexRune(s, '?'); pos > -1 { // escape query data 111 | return s[0:pos+1] + url.QueryEscape(s[pos+1:]) 112 | } 113 | return s 114 | } 115 | 116 | // URLDecode decode url string. 117 | func URLDecode(s string) string { 118 | if pos := strings.IndexRune(s, '?'); pos > -1 { // un-escape query data 119 | qy, err := url.QueryUnescape(s[pos+1:]) 120 | if err == nil { 121 | return s[0:pos+1] + qy 122 | } 123 | } 124 | return s 125 | } 126 | 127 | // Unique value in the given array, slice. 128 | func Unique(val any) any { 129 | switch tv := val.(type) { 130 | case []int: 131 | mp := make(map[int]int) 132 | for _, sVal := range tv { 133 | mp[sVal] = 1 134 | } 135 | 136 | // no repeat value 137 | if len(tv) == len(mp) { 138 | return tv 139 | } 140 | 141 | var ns []int 142 | for sVal := range mp { 143 | ns = append(ns, sVal) 144 | } 145 | return ns 146 | case []int64: 147 | mp := make(map[int64]int) 148 | for _, sVal := range tv { 149 | mp[sVal] = 1 150 | } 151 | 152 | // no repeat value 153 | if len(tv) == len(mp) { 154 | return tv 155 | } 156 | 157 | var ns []int64 158 | for sVal := range mp { 159 | ns = append(ns, sVal) 160 | } 161 | return ns 162 | case []string: 163 | mp := make(map[string]int) 164 | for _, sVal := range tv { 165 | mp[sVal] = 1 166 | } 167 | 168 | // no repeat value 169 | if len(tv) == len(mp) { 170 | return tv 171 | } 172 | 173 | var ns []string 174 | for sVal := range mp { 175 | ns = append(ns, sVal) 176 | } 177 | return ns 178 | } 179 | 180 | return val 181 | } 182 | 183 | // Substr cut string 184 | func Substr(s string, pos, length int) string { 185 | return strutil.Substr(s, pos, length) 186 | } 187 | 188 | // Email filter, clear invalid chars. 189 | func Email(s string) string { 190 | return strutil.FilterEmail(s) 191 | } 192 | -------------------------------------------------------------------------------- /converters.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "errors" 5 | "strconv" 6 | "strings" 7 | "text/template" 8 | "time" 9 | 10 | "github.com/gookit/goutil/arrutil" 11 | "github.com/gookit/goutil/mathutil" 12 | "github.com/gookit/goutil/strutil" 13 | ) 14 | 15 | // Some alias methods. 16 | var ( 17 | Lower = strings.ToLower 18 | Upper = strings.ToUpper 19 | Title = strings.ToTitle 20 | 21 | // EscapeJS escape javascript string 22 | EscapeJS = template.JSEscapeString 23 | // EscapeHTML escape html string 24 | EscapeHTML = template.HTMLEscapeString 25 | // error for params 26 | errInvalidParam = errors.New("invalid input parameter") 27 | ) 28 | 29 | /************************************************************* 30 | * value to int,bool,float,string 31 | *************************************************************/ 32 | 33 | // Int convert string to int 34 | func Int(in any) (int, error) { return ToInt(in) } 35 | 36 | // MustInt convert string to int, alias of the mathutil.SafeInt 37 | func MustInt(in any) int { 38 | return mathutil.SafeInt(in) 39 | } 40 | 41 | // ToInt convert string to int 42 | func ToInt(in any) (int, error) { return mathutil.ToInt(in) } 43 | 44 | // Int64 convert value to int64 45 | func Int64(in any) (int64, error) { return ToInt64(in) } 46 | 47 | // ToInt64 convert value to int64 48 | func ToInt64(val any) (int64, error) { return mathutil.ToInt64(val) } 49 | 50 | // MustInt64 convert value to int64, alias of the mathutil.SafeInt64 51 | func MustInt64(in any) int64 { 52 | return mathutil.SafeInt64(in) 53 | } 54 | 55 | // Uint convert string to uint 56 | func Uint(in any) (uint, error) { return ToUint(in) } 57 | 58 | // ToUint convert string to uint 59 | func ToUint(in any) (uint, error) { return mathutil.ToUint(in) } 60 | 61 | // MustUint convert string to uint, will ignore error 62 | func MustUint(in any) uint { 63 | return mathutil.SafeUint(in) 64 | } 65 | 66 | // Uint64 convert string to uint64 67 | func Uint64(in any) (uint64, error) { return ToUint64(in) } 68 | 69 | // ToUint64 convert string to uint64 70 | func ToUint64(in any) (uint64, error) { return mathutil.ToUint64(in) } 71 | 72 | // MustUint64 convert string to uint64, alias of the mathutil.SafeUint64 73 | func MustUint64(in any) uint64 { 74 | return mathutil.SafeUint64(in) 75 | } 76 | 77 | // Float convert string to float 78 | func Float(s string) (float64, error) { return ToFloat(s) } 79 | 80 | // ToFloat convert string to float 81 | func ToFloat(s string) (float64, error) { 82 | return strconv.ParseFloat(Trim(s), 0) 83 | } 84 | 85 | // MustFloat convert string to float, will ignore error 86 | func MustFloat(s string) float64 { 87 | val, _ := strconv.ParseFloat(Trim(s), 0) 88 | return val 89 | } 90 | 91 | // ToBool convert string to bool 92 | func ToBool(s string) (bool, error) { return Bool(s) } 93 | 94 | // Bool parse string to bool 95 | func Bool(s string) (bool, error) { return strutil.ToBool(s) } 96 | 97 | // MustBool convert, will ignore error. 98 | func MustBool(s string) bool { 99 | val, _ := Bool(Trim(s)) 100 | return val 101 | } 102 | 103 | // String convert val to string 104 | func String(val any) (string, error) { return ToString(val) } 105 | 106 | // MustString convert value to string, will ignore error 107 | func MustString(in any) string { 108 | return strutil.SafeString(in) 109 | } 110 | 111 | // ToString convert value to string 112 | func ToString(val any) (string, error) { return strutil.ToString(val) } 113 | 114 | /************************************************************* 115 | * change string case 116 | *************************************************************/ 117 | 118 | // Lowercase alias of the strings.ToLower() 119 | func Lowercase(s string) string { return strings.ToLower(s) } 120 | 121 | // Uppercase alias of the strings.ToUpper() 122 | func Uppercase(s string) string { return strings.ToUpper(s) } 123 | 124 | // UpperWord Change the first character of each word to uppercase 125 | func UpperWord(s string) string { return strutil.UpperWord(s) } 126 | 127 | // LowerFirst lower first char 128 | func LowerFirst(s string) string { return strutil.LowerFirst(s) } 129 | 130 | // UpperFirst upper first char 131 | func UpperFirst(s string) string { return strutil.UpperFirst(s) } 132 | 133 | // Snake alias of the SnakeCase 134 | func Snake(s string, sep ...string) string { return SnakeCase(s, sep...) } 135 | 136 | // SnakeCase convert. eg "RangePrice" -> "range_price" 137 | func SnakeCase(s string, sep ...string) string { return strutil.SnakeCase(s, sep...) } 138 | 139 | // Camel alias of the CamelCase 140 | func Camel(s string, sep ...string) string { return strutil.CamelCase(s, sep...) } 141 | 142 | // CamelCase convert string to camel case. 143 | // 144 | // Support: 145 | // 146 | // "range_price" -> "rangePrice" 147 | // "range price" -> "rangePrice" 148 | // "range-price" -> "rangePrice" 149 | func CamelCase(s string, sep ...string) string { return strutil.CamelCase(s, sep...) } 150 | 151 | /************************************************************* 152 | * string to slice, time 153 | *************************************************************/ 154 | 155 | // StrToInts split string to slice and convert item to int. 156 | func StrToInts(s string, sep ...string) ([]int, error) { return strutil.ToIntSlice(s, sep...) } 157 | 158 | // StrToArray alias of the StrToSlice() 159 | func StrToArray(s string, sep ...string) []string { return StrToSlice(s, sep...) } 160 | 161 | // StrToSlice split string to array. 162 | func StrToSlice(s string, sep ...string) []string { 163 | if len(sep) > 0 { 164 | return strutil.Split(s, sep[0]) 165 | } 166 | return strutil.Split(s, ",") 167 | } 168 | 169 | // StringsToInts string slice to int slice 170 | func StringsToInts(ss []string) ([]int, error) { return arrutil.StringsToInts(ss) } 171 | 172 | // StrToTime convert date string to time.Time 173 | func StrToTime(s string, layouts ...string) (time.Time, error) { 174 | return strutil.ToTime(s, layouts...) 175 | } 176 | -------------------------------------------------------------------------------- /filtration_test.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/gookit/goutil/testutil/assert" 9 | ) 10 | 11 | func TestFiltration(t *testing.T) { 12 | is := assert.New(t) 13 | 14 | fl := New(map[string]any{ 15 | "key0": " abc ", 16 | "key1": "2", 17 | "sub": map[string]string{"k0": "v0"}, 18 | "sub1": map[string]any{"k0": "v0"}, 19 | "sub2": map[any]any{"k0": "v0"}, 20 | }) 21 | 22 | is.Eq("strToTime", Name("str2time")) 23 | is.Eq("some", Name("some")) 24 | 25 | is.Eq("", fl.String("key0")) 26 | is.Eq(0, fl.Int("key1")) 27 | 28 | val, ok := fl.Get("key1") 29 | is.True(ok) 30 | is.Eq("2", val) 31 | 32 | _, ok = fl.Get("sub.not-exist") 33 | is.False(ok) 34 | val, ok = fl.Get("sub.k0") 35 | is.True(ok) 36 | is.Eq("v0", val) 37 | 38 | val, ok = fl.Safe("key1") 39 | is.False(ok) 40 | is.Eq(nil, val) 41 | 42 | _, ok = fl.Raw("sub1.not-exist") 43 | is.False(ok) 44 | val, ok = fl.Raw("sub1.k0") 45 | is.True(ok) 46 | is.Eq("v0", val) 47 | 48 | val, ok = fl.Raw("sub2.k0") 49 | is.True(ok) 50 | is.Eq("v0", val) 51 | 52 | val, ok = fl.Raw("key1") 53 | is.True(ok) 54 | is.Eq("2", val) 55 | val, ok = fl.Raw("not-exist") 56 | is.False(ok) 57 | is.Eq(nil, val) 58 | 59 | f := New(map[string]any{ 60 | "key0": "34", 61 | "name": " inhere ", 62 | "email": " my@email.com ", 63 | "ids": " 1,2, 3", 64 | "jsCode": "", 65 | "htmlCode": "
some text
", 66 | "strings": []string{" a", " b ", "c "}, 67 | }) 68 | f.AddRules(map[string]string{ 69 | "ids": "strToInts", 70 | "key0": "int64", 71 | "email": "email", 72 | "name": "trim|ucFirst", 73 | "jsCode": "escapeJS", 74 | "htmlCode": "escapeHTML", 75 | "strings": "trimStrings", 76 | }) 77 | 78 | is.Nil(f.Sanitize()) 79 | is.NotEmpty(f.CleanData()) 80 | is.Eq(int64(34), f.SafeVal("key0")) 81 | is.Eq([]int{1, 2, 3}, f.SafeVal("ids")) 82 | is.Eq([]string{"a", "b", "c"}, f.SafeVal("strings")) 83 | is.Eq("Inhere", f.String("name")) 84 | is.Eq("my@email.com", f.String("email")) 85 | is.Eq("<p>some text</p>", f.SafeVal("htmlCode")) 86 | // < 1.15 \x3Cscript\x3Evar a = 23;\x3C/script\x3E 87 | // >= 1.15 \u003Cscript\u003Evar a \u003D 23;\u003C/script\u003E 88 | // is.Eq(`\x3Cscript\x3Evar a = 23;\x3C/script\x3E`, f.SafeVal("jsCode")) 89 | is.NotEq("", f.SafeVal("jsCode")) 90 | 91 | // clear all 92 | f.Clear() 93 | is.Empty(f.CleanData()) 94 | } 95 | 96 | func TestFiltration_AddRule(t *testing.T) { 97 | is := assert.New(t) 98 | 99 | f := New(nil) 100 | f.LoadData(map[string]any{ 101 | "name": " INHERE ", 102 | "age": "50 ", 103 | }) 104 | 105 | is.Panics(func() { 106 | f.AddRule("", nil) 107 | }) 108 | is.Panics(func() { 109 | f.AddRule("name", "") 110 | }) 111 | is.Panics(func() { 112 | f.AddRule("name", []int{1}) 113 | }) 114 | 115 | f.AddRule("name", func(v any) (any, error) { 116 | return strings.TrimSpace(v.(string)), nil 117 | }) 118 | 119 | is.NoErr(f.Filtering()) 120 | is.Eq("INHERE", f.String("name")) 121 | 122 | is.Len(f.CleanData(), 1) 123 | is.Contains(f.CleanData(), "name") 124 | 125 | // clear rules and cleanData 126 | f.ResetRules() 127 | is.Empty(f.CleanData()) 128 | is.NotEmpty(f.RawData()) 129 | 130 | f.AddRule("name", "trim|lower") 131 | is.NoErr(f.Filtering()) 132 | is.Eq("inhere", f.String("name")) 133 | 134 | // clear all data 135 | is.NotEmpty(f.CleanData()) 136 | f.ResetData(true) 137 | is.Empty(f.RawData()) 138 | is.Empty(f.CleanData()) 139 | f.LoadData(map[string]any{ 140 | "name": " Inhere0 ", 141 | }) 142 | is.NoErr(f.Filtering()) 143 | is.Eq("inhere0", f.String("name")) 144 | is.Eq("", f.String("not-exist")) 145 | 146 | f.ResetRules() 147 | f.AddRule("not-exist", "trim").SetDefaultVal(" def val ") 148 | is.NoErr(f.Filtering()) 149 | is.Eq("def val", f.String("not-exist")) 150 | 151 | // trimStrings error 152 | f = New(map[string]any{ 153 | "ints": []int{1, 2, 3}, 154 | }) 155 | f.AddRule("ints", "trimStrings") 156 | is.Err(f.Filtering()) 157 | is.Eq("invalid input parameter", f.Err().Error()) 158 | 159 | // stringsToInts error 160 | f.ResetRules() 161 | f.AddRule("ints", "stringsToInts") 162 | is.Err(f.Filtering()) 163 | is.Eq("invalid input parameter", f.Err().Error()) 164 | } 165 | 166 | func TestFiltration_Filtering(t *testing.T) { 167 | is := assert.New(t) 168 | 169 | data := map[string]any{ 170 | "name": "inhere", 171 | "age": "50", 172 | "money": "50.34", 173 | "remember": "yes", 174 | // 175 | "sub": map[string]string{"k0": "v0"}, 176 | "sub1": []string{"1", "2"}, 177 | "tags": "go;lib", 178 | "str1": " word ", 179 | "str2": "HELLO", 180 | "ids": []int{1, 2, 2, 1}, 181 | } 182 | f := New(data) 183 | f.AddRule("name", "upper") 184 | f.AddRule("age", "int") 185 | f.AddRule("money", "float") 186 | f.AddRule("remember", "bool") 187 | f.AddRule("sub1", "strings2ints") 188 | f.AddRule("tags", "str2arr:;") 189 | f.AddRule("ids", "unique") 190 | f.AddRule("str1", "ltrim|rtrim") 191 | f.AddRule("not-exist", "unique") 192 | f.AddRule("str2", "lower") 193 | 194 | is.Nil(f.Filtering()) 195 | is.Nil(f.Filtering()) 196 | is.True(f.IsOK()) 197 | 198 | // get value 199 | is.True(f.Bool("remember")) 200 | is.False(f.Bool("not-exist")) 201 | is.Eq(50, f.Int("age")) 202 | is.Eq(0, f.Int("not-exist")) 203 | is.Eq(50, f.MustGet("age")) 204 | is.Eq(int64(50), f.Int64("age")) 205 | is.Eq(int64(0), f.Int64("not-exist")) 206 | is.Eq(50.34, f.MustGet("money")) 207 | is.Eq([]int{1, 2}, f.MustGet("sub1")) 208 | is.Len(f.MustGet("ids"), 2) 209 | is.Eq([]string{"go", "lib"}, f.MustGet("tags")) 210 | is.Eq("INHERE", f.CleanData()["name"]) 211 | is.Eq("word", f.String("str1")) 212 | is.Eq("hello", f.String("str2")) 213 | 214 | f = New(data) 215 | f.AddRule("name", "int") 216 | is.Err(f.Sanitize()) 217 | 218 | data["name"] = " inhere " 219 | data["sDate"] = "2018-10-16 12:34" 220 | data["msg"] = " hello world " 221 | data["msg1"] = "helloWorld" 222 | data["msg2"] = "hello_world" 223 | f = New(data) 224 | f.AddRules(map[string]string{ 225 | "age": "uint", 226 | "money": "float", 227 | "name": "trim|ucFirst", 228 | "str1": "trim|upper", 229 | "sDate": "str2time", 230 | "msg": "trim|ucWord", 231 | "msg1": "snake", 232 | "msg2": "camel", 233 | "str2": "lowerFirst", 234 | }) 235 | is.Nil(f.Sanitize()) 236 | is.Eq("Inhere", f.String("name")) 237 | is.Eq("WORD", f.String("str1")) 238 | is.Eq("Hello World", f.String("msg")) 239 | is.Eq("hello_world", f.String("msg1")) 240 | is.Eq("helloWorld", f.String("msg2")) 241 | is.Eq("hELLO", f.String("str2")) 242 | 243 | sTime, ok := f.Safe("sDate") 244 | is.True(ok) 245 | is.StrContains(fmt.Sprintf("%v", sTime), "2018-10-16 12:34:00") 246 | 247 | data["url"] = "a.com?p=1" 248 | f = New(data) 249 | f.AddRule("url", "urlEncode") 250 | f.AddRule("msg1", "substr:0,2") 251 | is.Nil(f.Sanitize()) 252 | is.Eq("he", f.String("msg1")) 253 | is.Eq("a.com?p%3D1", f.String("url")) 254 | 255 | // bind 256 | f = New(map[string]any{ 257 | "name": " inhere ", 258 | "age": " 89 ", 259 | }) 260 | f.AddRules(map[string]string{ 261 | "age": "trim|int", 262 | "name": "trim|ucFirst", 263 | }) 264 | is.Nil(f.Filtering()) 265 | user := &struct { 266 | Age int 267 | Name string 268 | }{} 269 | err := f.BindStruct(user) 270 | is.Nil(err) 271 | is.Eq(89, user.Age) 272 | is.Eq("Inhere", user.Name) 273 | } 274 | -------------------------------------------------------------------------------- /converters_test.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/gookit/goutil/testutil/assert" 9 | ) 10 | 11 | func TestValToInt(t *testing.T) { 12 | is := assert.New(t) 13 | 14 | tests := []any{ 15 | 2, 16 | int8(2), int16(2), int32(2), int64(2), 17 | uint(2), uint8(2), uint16(2), uint32(2), uint64(2), 18 | float32(2.2), 2.3, 19 | "2", 20 | } 21 | 22 | // To int 23 | intVal, err := Int("2") 24 | is.Nil(err) 25 | is.Eq(2, intVal) 26 | 27 | intVal, err = ToInt("-2") 28 | is.Nil(err) 29 | is.Eq(-2, intVal) 30 | 31 | is.Eq(-2, MustInt("-2")) 32 | is.Eq(0, MustInt("2a")) 33 | is.Eq(0, MustInt(nil)) 34 | for _, in := range tests { 35 | is.Eq(2, MustInt(in)) 36 | } 37 | 38 | // To int64 39 | i64Val, err := ToInt64("2") 40 | is.Nil(err) 41 | is.Eq(int64(2), i64Val) 42 | 43 | i64Val, err = Int64("-2") 44 | is.Nil(err) 45 | is.Eq(int64(-2), i64Val) 46 | 47 | is.Eq(int64(0), MustInt64("2a")) 48 | is.Eq(int64(0), MustInt64(nil)) 49 | for _, in := range tests { 50 | is.Eq(int64(2), MustInt64(in)) 51 | } 52 | 53 | // To uint 54 | uintVal, err := Uint("2") 55 | is.Nil(err) 56 | is.Eq(uint(2), uintVal) 57 | 58 | _, err = ToUint("-2") 59 | is.Err(err) 60 | 61 | is.Eq(uint(0), MustUint("-2")) 62 | is.Eq(uint(0), MustUint("2a")) 63 | is.Eq(uint(0), MustUint(nil)) 64 | for _, in := range tests { 65 | is.Eq(uint(2), MustUint(in)) 66 | } 67 | 68 | // To uint64 69 | u64, err := Uint64("2") 70 | is.Nil(err) 71 | is.Eq(uint64(2), u64) 72 | 73 | _, err = ToUint64("-2") 74 | is.Err(err) 75 | 76 | is.Eq(uint64(0), MustUint64("-2")) 77 | is.Eq(uint64(0), MustUint64("2a")) 78 | is.Eq(uint64(0), MustUint64(nil)) 79 | for _, in := range tests { 80 | is.Eq(uint64(2), MustUint64(in)) 81 | } 82 | 83 | } 84 | 85 | func TestValToStr(t *testing.T) { 86 | is := assert.New(t) 87 | 88 | tests := []any{ 89 | 2, 90 | int8(2), int16(2), int32(2), int64(2), 91 | uint(2), uint8(2), uint16(2), uint32(2), uint64(2), 92 | "2", 93 | } 94 | for _, in := range tests { 95 | is.Eq("2", MustString(in)) 96 | } 97 | 98 | tests1 := []any{ 99 | float32(2.3), 2.3, 100 | } 101 | for _, in := range tests1 { 102 | is.Eq("2.3", MustString(in)) 103 | } 104 | 105 | str, err := String(2.3) 106 | is.NoErr(err) 107 | is.Eq("2.3", str) 108 | 109 | str, err = String(nil) 110 | is.NoErr(err) 111 | is.Eq("", str) 112 | 113 | _, err = String([]string{"a"}) 114 | is.Err(err) 115 | } 116 | 117 | func TestStrToFloat(t *testing.T) { 118 | is := assert.New(t) 119 | 120 | is.Eq(123.5, MustFloat("123.5")) 121 | is.Eq(float64(0), MustFloat("invalid")) 122 | 123 | fltVal, err := ToFloat("123.5") 124 | is.Nil(err) 125 | is.Eq(123.5, fltVal) 126 | 127 | fltVal, err = Float("-123.5") 128 | is.Nil(err) 129 | is.Eq(-123.5, fltVal) 130 | } 131 | 132 | func TestStrToBool(t *testing.T) { 133 | is := assert.New(t) 134 | 135 | tests1 := map[string]bool{ 136 | "1": true, 137 | "on": true, 138 | "yes": true, 139 | "true": true, 140 | "false": false, 141 | "off": false, 142 | "no": false, 143 | "0": false, 144 | } 145 | 146 | for str, want := range tests1 { 147 | is.Eq(want, MustBool(str)) 148 | } 149 | 150 | blVal, err := ToBool("1") 151 | is.Nil(err) 152 | is.True(blVal) 153 | 154 | blVal, err = Bool("10") 155 | is.Err(err) 156 | is.False(blVal) 157 | } 158 | 159 | func TestLowerOrUpperFirst(t *testing.T) { 160 | is := assert.New(t) 161 | 162 | // Uppercase, Lowercase 163 | is.Eq("ABC", Uppercase("abc")) 164 | is.Eq("ABC", Upper("abc")) 165 | is.Eq("abc", Lowercase("ABC")) 166 | is.Eq("abc", Lower("ABC")) 167 | 168 | tests := []string{ 169 | "Abc-abc", 170 | "ABC-aBC", 171 | } 172 | 173 | // UpperFirst, LowerFirst 174 | for _, sample := range tests { 175 | ss := strings.Split(sample, "-") 176 | is.Eq(ss[0], UpperFirst(ss[1])) 177 | is.Eq(ss[1], LowerFirst(ss[0])) 178 | } 179 | 180 | is.Eq("", LowerFirst("")) 181 | is.Eq("", UpperFirst("")) 182 | is.Eq("abc", LowerFirst("abc")) 183 | is.Eq("Abc", UpperFirst("Abc")) 184 | 185 | // UpperWord 186 | is.Eq("", UpperWord("")) 187 | is.Eq("Hello World!", UpperWord("hello world!")) 188 | } 189 | 190 | func TestSnakeCase(t *testing.T) { 191 | is := assert.New(t) 192 | tests := map[string]string{ 193 | "RangePrice": "range_price", 194 | "rangePrice": "range_price", 195 | "range_price": "range_price", 196 | } 197 | 198 | for sample, want := range tests { 199 | is.Eq(want, SnakeCase(sample)) 200 | } 201 | 202 | is.Eq("range-price", Snake("rangePrice", "-")) 203 | is.Eq("range price", SnakeCase("rangePrice", " ")) 204 | } 205 | 206 | func TestCamelCase(t *testing.T) { 207 | is := assert.New(t) 208 | tests := map[string]string{ 209 | "rangePrice": "rangePrice", 210 | "range_price": "rangePrice", 211 | "_range_price": "RangePrice", 212 | } 213 | 214 | for sample, want := range tests { 215 | is.Eq(want, CamelCase(sample)) 216 | } 217 | 218 | is.Eq("rangePrice", Camel("range-price", "-")) 219 | is.Eq("rangePrice", CamelCase("range price", " ")) 220 | 221 | // custom sep char 222 | is.Eq("rangePrice", CamelCase("range+price", "+")) 223 | is.Eq("rangePrice", CamelCase("range*price", "*")) 224 | } 225 | 226 | func TestStrToInts(t *testing.T) { 227 | is := assert.New(t) 228 | 229 | ints, err := StrToInts("a,b,c") 230 | is.Err(err) 231 | is.Len(ints, 0) 232 | 233 | ints, err = StrToInts("1,2,3") 234 | is.Nil(err) 235 | is.Eq([]int{1, 2, 3}, ints) 236 | } 237 | 238 | func TestStr2Array(t *testing.T) { 239 | is := assert.New(t) 240 | 241 | ss := StrToArray("a,b,c", ",") 242 | is.Len(ss, 3) 243 | is.Eq(`[]string{"a", "b", "c"}`, fmt.Sprintf("%#v", ss)) 244 | 245 | tests := []string{ 246 | // sample 247 | "a,b,c", 248 | "a,b,c,", 249 | ",a,b,c", 250 | "a, b,c", 251 | "a,,b,c", 252 | "a, , b,c", 253 | } 254 | 255 | for _, sample := range tests { 256 | ss = StrToArray(sample) 257 | is.Eq(`[]string{"a", "b", "c"}`, fmt.Sprintf("%#v", ss)) 258 | } 259 | 260 | ss = StrToSlice("", ",") 261 | is.Len(ss, 0) 262 | 263 | ss = StrToArray(", , ", ",") 264 | is.Len(ss, 0) 265 | } 266 | 267 | func TestStringsToInts(t *testing.T) { 268 | is := assert.New(t) 269 | 270 | ints, err := StringsToInts([]string{"1", "2"}) 271 | is.Nil(err) 272 | is.Eq("[]int{1, 2}", fmt.Sprintf("%#v", ints)) 273 | 274 | _, err = StringsToInts([]string{"a", "b"}) 275 | is.Err(err) 276 | } 277 | 278 | func TestEscape(t *testing.T) { 279 | tests := struct{ give, want string }{ 280 | "some text
", 281 | "<p>some text</p>", 282 | } 283 | 284 | assert.Eq(t, tests.want, EscapeHTML(tests.give)) 285 | 286 | tests = struct{ give, want string }{ 287 | "", 288 | `\x3Cscript\x3Evar a = 23;\x3C/script\x3E`, 289 | } 290 | assert.NotEq(t, tests.give, EscapeJS(tests.give)) 291 | } 292 | 293 | func TestStrToTime(t *testing.T) { 294 | is := assert.New(t) 295 | tests := map[string]string{ 296 | "20180927": "2018-09-27 00:00:00", 297 | "2018-09-27": "2018-09-27 00:00:00", 298 | "2018-09-27 12": "2018-09-27 12:00:00", 299 | "2018-09-27T12": "2018-09-27 12:00:00", 300 | "2018-09-27 12:34": "2018-09-27 12:34:00", 301 | "2018-09-27T12:34": "2018-09-27 12:34:00", 302 | "2018-09-27 12:34:45": "2018-09-27 12:34:45", 303 | "2018-09-27T12:34:45": "2018-09-27 12:34:45", 304 | "2018/09/27 12:34:45": "2018-09-27 12:34:45", 305 | "2018/09/27T12:34:45Z": "2018-09-27 12:34:45", 306 | } 307 | 308 | for sample, want := range tests { 309 | tm, err := StrToTime(sample) 310 | is.Nil(err) 311 | is.StrContains(tm.String(), want) 312 | } 313 | 314 | tm, err := StrToTime("invalid") 315 | is.Err(err) 316 | is.True(tm.IsZero()) 317 | 318 | tm, err = StrToTime("2018-09-27T15:34", "2018-09-27 15:34:23") 319 | is.Err(err) 320 | is.True(tm.IsZero()) 321 | } 322 | -------------------------------------------------------------------------------- /filtration.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/gookit/goutil/maputil" 9 | "github.com/gookit/goutil/strutil" 10 | ) 11 | 12 | // Filtration definition. Sanitization Sanitizing Sanitize 13 | type Filtration struct { 14 | err error 15 | // raw data 16 | data map[string]any 17 | // mark has apply filters 18 | filtered bool 19 | // filtered and clean data 20 | cleanData map[string]any 21 | // filter rules 22 | filterRules []*Rule 23 | } 24 | 25 | // New a Filtration 26 | func New(data map[string]any) *Filtration { 27 | return &Filtration{ 28 | data: data, 29 | // init map 30 | cleanData: make(map[string]any), 31 | } 32 | } 33 | 34 | // LoadData set raw data for filtering. 35 | func (f *Filtration) LoadData(data map[string]any) { 36 | f.data = data 37 | } 38 | 39 | // ResetData reset raw and filtered data 40 | func (f *Filtration) ResetData(resetRaw bool) { 41 | f.err = nil 42 | f.filtered = false 43 | 44 | // reset data. 45 | if resetRaw { 46 | f.data = make(map[string]any) 47 | } 48 | 49 | f.cleanData = make(map[string]any) 50 | } 51 | 52 | // ResetRules reset rules and filtered data 53 | func (f *Filtration) ResetRules() { 54 | f.err = nil 55 | f.filtered = false 56 | 57 | // clear rules 58 | f.filterRules = f.filterRules[:0] 59 | 60 | // clear cleanData 61 | f.cleanData = make(map[string]any) 62 | } 63 | 64 | // Clear all data and rules 65 | func (f *Filtration) Clear() { 66 | f.data = make(map[string]any) 67 | f.ResetRules() 68 | } 69 | 70 | /************************************************************* 71 | * add rules and filtering data 72 | *************************************************************/ 73 | 74 | // AddRule add filter(s) rule. 75 | // 76 | // Usage: 77 | // 78 | // f.AddRule("name", "trim") 79 | // f.AddRule("age", "int") 80 | // f.AddRule("age", "trim|int") 81 | func (f *Filtration) AddRule(field string, rule any) *Rule { 82 | fields := strutil.Split(field, ",") 83 | if len(fields) == 0 { 84 | panic("filter: invalid fields parameters, cannot be empty") 85 | } 86 | 87 | r := newRule(fields) 88 | 89 | if strRule, ok := rule.(string); ok { 90 | strRule = strings.TrimSpace(strRule) 91 | rules := strutil.Split(strings.Trim(strRule, "|:"), "|") 92 | 93 | if len(rules) == 0 { 94 | panic("filter: invalid 'rule' params, cannot be empty") 95 | } 96 | 97 | r.AddFilters(rules...) 98 | } else if fn, ok := rule.(func(any) (any, error)); ok { 99 | r.SetFilterFunc(fn) 100 | } else { 101 | panic("filter: 'rule' params cannot be empty and type allow: string, func") 102 | } 103 | 104 | f.filterRules = append(f.filterRules, r) 105 | return r 106 | } 107 | 108 | // AddRules add multi rules. 109 | // 110 | // Usage: 111 | // 112 | // f.AddRules(map[string]string{ 113 | // "name": "trim|lower", 114 | // "age": "trim|int", 115 | // }) 116 | func (f *Filtration) AddRules(rules map[string]string) *Filtration { 117 | for field, rule := range rules { 118 | f.AddRule(field, rule) 119 | } 120 | return f 121 | } 122 | 123 | // Sanitize is alias of the Filtering() 124 | func (f *Filtration) Sanitize() error { 125 | return f.Filtering() 126 | } 127 | 128 | // Filtering apply all filter rules, filtering data 129 | func (f *Filtration) Filtering() error { 130 | if f.filtered || f.err != nil { 131 | return f.err 132 | } 133 | 134 | // apply rule to validate data. 135 | for _, rule := range f.filterRules { 136 | if err := rule.Apply(f); err != nil { // has error 137 | f.err = err 138 | break 139 | } 140 | } 141 | 142 | f.filtered = true 143 | return f.err 144 | } 145 | 146 | // IsOK of to apply filters 147 | func (f *Filtration) IsOK() bool { 148 | return f.err == nil 149 | } 150 | 151 | // Err get error 152 | func (f *Filtration) Err() error { 153 | return f.err 154 | } 155 | 156 | /************************************************************* 157 | * get raw/filtered data value 158 | *************************************************************/ 159 | 160 | // Raw get raw value by key 161 | func (f *Filtration) Raw(key string) (any, bool) { 162 | return maputil.GetByPath(key, f.data) 163 | } 164 | 165 | // Safe get filtered value by key 166 | func (f *Filtration) Safe(key string) (any, bool) { 167 | return maputil.GetByPath(key, f.cleanData) 168 | } 169 | 170 | // SafeVal get filtered value by key 171 | func (f *Filtration) SafeVal(key string) any { 172 | val, _ := maputil.GetByPath(key, f.cleanData) 173 | return val 174 | } 175 | 176 | // Get value by key 177 | func (f *Filtration) Get(key string) (any, bool) { 178 | val, ok := maputil.GetByPath(key, f.cleanData) 179 | if !ok { 180 | val, ok = maputil.GetByPath(key, f.data) 181 | } 182 | 183 | return val, ok 184 | } 185 | 186 | // MustGet value by key 187 | func (f *Filtration) MustGet(key string) any { 188 | val, _ := f.Get(key) 189 | return val 190 | } 191 | 192 | // Int get a int value from filtered data. 193 | func (f *Filtration) Int(key string) int { 194 | if val, ok := f.Safe(key); ok { 195 | return MustInt(val) 196 | } 197 | return 0 198 | } 199 | 200 | // Int64 get a int value from filtered data. 201 | func (f *Filtration) Int64(key string) int64 { 202 | if val, ok := f.Safe(key); ok { 203 | return MustInt64(val) 204 | } 205 | return 0 206 | } 207 | 208 | // Bool value get from the filtered data. 209 | func (f *Filtration) Bool(key string) bool { 210 | if val, ok := f.Safe(key); ok { 211 | return val.(bool) 212 | } 213 | 214 | return false 215 | } 216 | 217 | // String get a string value from filtered data. 218 | func (f *Filtration) String(key string) string { 219 | val, ok := f.Safe(key) 220 | if !ok { 221 | return "" 222 | } 223 | 224 | // is string. 225 | if str, ok := val.(string); ok { 226 | return str 227 | } 228 | return fmt.Sprint(val) 229 | } 230 | 231 | // BindStruct bind the filtered data to struct. 232 | func (f *Filtration) BindStruct(ptr any) error { 233 | bts, err := json.Marshal(f.cleanData) 234 | if err != nil { 235 | return err 236 | } 237 | 238 | return json.Unmarshal(bts, ptr) 239 | } 240 | 241 | // RawData get raw data 242 | func (f *Filtration) RawData() map[string]any { 243 | return f.data 244 | } 245 | 246 | // CleanData get filtered data 247 | func (f *Filtration) CleanData() map[string]any { 248 | return f.cleanData 249 | } 250 | 251 | /************************************************************* 252 | * filtering rule 253 | *************************************************************/ 254 | 255 | // Rule definition 256 | type Rule struct { 257 | // fields to filter 258 | fields []string 259 | // filter name list 260 | filters []string 261 | // filter args. { index: "args" } 262 | filterArgs map[int]string 263 | // user custom filter func 264 | filterFunc func(val any) (any, error) 265 | // default value for the rule 266 | defaultVal any 267 | } 268 | 269 | func newRule(fields []string) *Rule { 270 | return &Rule{ 271 | fields: fields, 272 | // init map 273 | filterArgs: make(map[int]string), 274 | } 275 | } 276 | 277 | // SetDefaultVal set default value for the rule 278 | func (r *Rule) SetDefaultVal(defaultVal any) *Rule { 279 | r.defaultVal = defaultVal 280 | return r 281 | } 282 | 283 | // SetFilterFunc user custom filter func 284 | func (r *Rule) SetFilterFunc(fn func(val any) (any, error)) *Rule { 285 | r.filterFunc = fn 286 | return r 287 | } 288 | 289 | // AddFilters add multi filter(s). 290 | // 291 | // Usage: 292 | // 293 | // r.AddFilters("int", "str2arr:,") 294 | func (r *Rule) AddFilters(filters ...string) *Rule { 295 | for _, filterName := range filters { 296 | pos := strings.IndexRune(filterName, ':') 297 | if pos > 0 { // has filter args 298 | name := filterName[:pos] 299 | index := len(r.filters) 300 | r.filters = append(r.filters, name) 301 | r.filterArgs[index] = filterName[pos+1:] 302 | } else { 303 | r.filters = append(r.filters, filterName) 304 | } 305 | } 306 | 307 | return r 308 | } 309 | 310 | // Apply rule for the rule fields 311 | func (r *Rule) Apply(f *Filtration) (err error) { 312 | // validate field 313 | for _, field := range r.Fields() { 314 | // get field value. 315 | val, has := f.Get(field) 316 | if !has { // no field 317 | if r.defaultVal == nil { 318 | continue 319 | } 320 | 321 | // has default value 322 | val = r.defaultVal 323 | } 324 | 325 | // custom filter func 326 | if r.filterFunc != nil { 327 | val, err = r.filterFunc(val) 328 | if err != nil { 329 | return err 330 | } 331 | 332 | // save filtered value. 333 | f.cleanData[field] = val 334 | continue 335 | } 336 | 337 | // call built-in filters 338 | for i, name := range r.filters { 339 | args := parseArgString(r.filterArgs[i]) 340 | val, err = Apply(name, val, args) 341 | if err != nil { 342 | return err 343 | } 344 | } 345 | 346 | // save filtered value. 347 | f.cleanData[field] = val 348 | } 349 | 350 | return 351 | } 352 | 353 | // Fields name get 354 | func (r *Rule) Fields() []string { 355 | return r.fields 356 | } 357 | --------------------------------------------------------------------------------