├── .github └── workflows │ └── workflow.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── _examples ├── full │ └── main.go └── mold │ └── main.go ├── cache.go ├── errors.go ├── field_level.go ├── go.mod ├── go.sum ├── modifiers ├── modifiers.go ├── multi.go ├── multi_test.go ├── string.go └── string_test.go ├── mold.go ├── mold_test.go ├── restricted.go ├── scrubbers ├── scrubbers.go ├── string.go ├── string_test.go └── util.go ├── struct_level.go └── util.go /.github/workflows/workflow.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | name: Test 7 | jobs: 8 | test: 9 | strategy: 10 | matrix: 11 | go-version: [1.23.x, 1.22.x, 1.21.x, 1.20.x, 1.19.x, 1.18.x] 12 | os: [ubuntu-latest, macos-latest, windows-latest] 13 | runs-on: ${{ matrix.os }} 14 | steps: 15 | - name: Install Go 16 | uses: actions/setup-go@v5 17 | with: 18 | go-version: ${{ matrix.go-version }} 19 | 20 | - name: Checkout code 21 | uses: actions/checkout@v4 22 | 23 | - name: Restore Cache 24 | uses: actions/cache@v4 25 | with: 26 | path: ~/go/pkg/mod 27 | key: ${{ runner.os }}-v1-go-${{ hashFiles('**/go.sum') }} 28 | restore-keys: | 29 | ${{ runner.os }}-v1-go- 30 | 31 | - name: Test 32 | run: go test -race -covermode=atomic -coverprofile="profile.cov" ./... 33 | 34 | - name: Send Coverage 35 | if: matrix.os == 'ubuntu-latest' && matrix.go-version == '1.23.x' 36 | uses: shogo82148/actions-goveralls@v1 37 | with: 38 | path-to-profile: profile.cov 39 | 40 | golangci: 41 | name: lint 42 | runs-on: ubuntu-latest 43 | steps: 44 | - uses: actions/checkout@v4 45 | - name: golangci-lint 46 | uses: golangci/golangci-lint-action@v6 47 | with: 48 | version: latest 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # IDEs 14 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Dean Karn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOCMD=GO111MODULE=on go 2 | 3 | lint: 4 | golangci-lint run 5 | 6 | test: 7 | $(GOCMD) test -cover -race ./... 8 | 9 | bench: 10 | $(GOCMD) test -bench=. -benchmem ./... 11 | 12 | .PHONY: test bench lint -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Package mold 2 | ============ 3 |  4 | [](https://travis-ci.org/go-playground/mold) 5 | [](https://coveralls.io/github/go-playground/mold?branch=v2) 6 | [](https://goreportcard.com/report/github.com/go-playground/mold) 7 | [](https://godoc.org/github.com/go-playground/mold) 8 |  9 | 10 | Package mold is a general library to help modify or set data within data structures and other objects. 11 | 12 | How can this help me you ask, please see the examples [here](_examples/full/main.go) 13 | 14 | Requirements 15 | ------------ 16 | - Go 1.18+ 17 | 18 | Installation 19 | ------------ 20 | 21 | Use go get. 22 | ```shell 23 | go get -u github.com/go-playground/mold/v4 24 | ``` 25 | 26 | Examples 27 | ---------- 28 | | Example | Description | 29 | |----------------------------------|--------------------------------------------------------------------| 30 | | [simple](_examples/mold/main.go) | A basic example with custom function. | 31 | | [full](_examples/full/main.go) | A more real life example combining the usage of multiple packages. | 32 | 33 | 34 | Modifiers 35 | ---------- 36 | These functions modify the data in-place. 37 | 38 | | Name | Description | 39 | |---------------------|-------------------------------------------------------------------------------------------| 40 | | camel | Camel Cases the data. | 41 | | default | Sets the provided default value only if the data is equal to it's default datatype value. | 42 | | empty | Sets the field equal to the datatype default value. e.g. 0 for int. | 43 | | lcase | lowercases the data. | 44 | | ltrim | Trims spaces from the left of the data provided in the params. | 45 | | rtrim | Trims spaces from the right of the data provided in the params. | 46 | | set | Set the provided value. | 47 | | slug | Converts the field to a [slug](https://github.com/gosimple/slug) | 48 | | snake | Snake Cases the data. | 49 | | strip_alpha | Strips all ascii characters from the data. | 50 | | strip_alpha_unicode | Strips all unicode characters from the data. | 51 | | strip_num | Strips all ascii numeric characters from the data. | 52 | | strip_num_unicode | Strips all unicode numeric characters from the data. | 53 | | strip_punctuation | Strips all ascii punctuation from the data. | 54 | | title | Title Cases the data. | 55 | | tprefix | Trims a prefix from the value using the provided param value. | 56 | | trim | Trims space from the data. | 57 | | tsuffix | Trims a suffix from the value using the provided param value. | 58 | | ucase | Uppercases the data. | 59 | | ucfirst | Upper cases the first character of the data. | 60 | 61 | **Special Notes:** 62 | `default` and `set` modifiers are special in that they can be used to set the value of a field or underlying type information or attributes and both use the same underlying function to set the data. 63 | 64 | Setting a Param will have the following special effects on data types where it's not just the value being set: 65 | - Chan - param used to set the buffer size, default = 0. 66 | - Slice - param used to set the capacity, default = 0. 67 | - Map - param used to set the size, default = 0. 68 | - time.Time - param used to set the time format OR value, default = time.Now(), `utc` = time.Now().UTC(), other tries to parse using RFC3339Nano and set a time value. 69 | 70 | Scrubbers 71 | ---------- 72 | These functions obfuscate the specified types within the data for pii purposes. 73 | 74 | | Name | Description | 75 | |--------|-------------------------------------------------------------------| 76 | | emails | Scrubs multiple emails from data. | 77 | | email | Scrubs the data from and specifies the sha name of the same name. | 78 | | text | Scrubs the data from and specifies the sha name of the same name. | 79 | | name | Scrubs the data from and specifies the sha name of the same name. | 80 | | fname | Scrubs the data from and specifies the sha name of the same name. | 81 | | lname | Scrubs the data from and specifies the sha name of the same name. | 82 | 83 | 84 | Special Information 85 | ------------------- 86 | - To use a comma(,) within your params replace use it's hex representation instead '0x2C' which will be replaced while caching. 87 | 88 | Contributing 89 | ------------ 90 | I am definitely interested in the communities help in adding more scrubbers and modifiers. 91 | Please send a PR with tests, and preferably no extra dependencies, at lease until a solid base 92 | has been built. 93 | 94 | Complimentary Software 95 | ---------------------- 96 | 97 | Here is a list of software that compliments using this library post decoding. 98 | 99 | * [validator](https://github.com/go-playground/validator) - Go Struct and Field validation, including Cross Field, Cross Struct, Map, Slice and Array diving. 100 | * [form](https://github.com/go-playground/form) - Decodes url.Values into Go value(s) and Encodes Go value(s) into url.Values. Dual Array and Full map support. 101 | 102 | License 103 | ------ 104 | Distributed under MIT License, please see license file in code for more details. 105 | -------------------------------------------------------------------------------- /_examples/full/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/url" 8 | 9 | "github.com/go-playground/form/v4" 10 | "github.com/go-playground/mold/v4/modifiers" 11 | "github.com/go-playground/mold/v4/scrubbers" 12 | "github.com/go-playground/validator/v10" 13 | ) 14 | 15 | // This example is centered around a form post, but doesn't have to be 16 | // just trying to give a well rounded real life example. 17 | 18 | //
29 | 30 | var ( 31 | conform = modifiers.New() 32 | scrub = scrubbers.New() 33 | validate = validator.New() 34 | decoder = form.NewDecoder() 35 | ) 36 | 37 | // Address contains address information 38 | type Address struct { 39 | Name string `mod:"trim" validate:"required"` 40 | Phone string `mod:"trim" validate:"required"` 41 | } 42 | 43 | // User contains user information 44 | type User struct { 45 | Name string `mod:"trim" validate:"required" scrub:"name"` 46 | Age uint8 ` validate:"required,gt=0,lt=130"` 47 | Gender string ` validate:"required"` 48 | Email string `mod:"trim" validate:"required,email" scrub:"emails"` 49 | Address []Address ` validate:"required,dive"` 50 | Active bool `form:"active"` 51 | Misc map[string]string `mod:"dive,keys,trim,endkeys,trim"` 52 | } 53 | 54 | func main() { 55 | // this simulates the results of http.Request's ParseForm() function 56 | values := parseForm() 57 | 58 | var user User 59 | 60 | // must pass a pointer 61 | err := decoder.Decode(&user, values) 62 | if err != nil { 63 | log.Panic(err) 64 | } 65 | fmt.Printf("Decoded:%+v\n\n", user) 66 | 67 | // great now lets conform our values, after all a human input the data 68 | // nobody's perfect 69 | err = conform.Struct(context.Background(), &user) 70 | if err != nil { 71 | log.Panic(err) 72 | } 73 | fmt.Printf("Conformed:%+v\n\n", user) 74 | 75 | // that's better all those extra spaces are gone 76 | // let's validate the data 77 | err = validate.Struct(user) 78 | if err != nil { 79 | log.Panic(err) 80 | } 81 | 82 | // ok now we know our data is good, let's do something with it like: 83 | // save to database 84 | // process request 85 | // etc.... 86 | 87 | // ok now I'm done working with my data 88 | // let's log or store it somewhere 89 | // oh wait a minute, we have some sensitive PII data 90 | // let's make sure that's de-identified first 91 | err = scrub.Struct(context.Background(), &user) 92 | if err != nil { 93 | log.Panic(err) 94 | } 95 | fmt.Printf("Scrubbed:%+v\n\n", user) 96 | } 97 | 98 | // this simulates the results of http.Request's ParseForm() function 99 | func parseForm() url.Values { 100 | return url.Values{ 101 | "Name": []string{" joeybloggs "}, 102 | "Age": []string{"3"}, 103 | "Gender": []string{"Male"}, 104 | "Email": []string{"Dean.Karn@gmail.com "}, 105 | "Address[0].Name": []string{"26 Here Blvd."}, 106 | "Address[0].Phone": []string{"9(999)999-9999"}, 107 | "Address[1].Name": []string{"26 There Blvd."}, 108 | "Address[1].Phone": []string{"1(111)111-1111"}, 109 | "active": []string{"true"}, 110 | "Misc[ b4 ]": []string{" b4 "}, 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /_examples/mold/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/go-playground/mold/v4" 9 | ) 10 | 11 | var tform *mold.Transformer 12 | 13 | func main() { 14 | tform = mold.New() 15 | tform.Register("set", transformMyData) 16 | 17 | type Test struct { 18 | String string `mold:"set"` 19 | } 20 | 21 | var tt Test 22 | 23 | err := tform.Struct(context.Background(), &tt) 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | fmt.Printf("%+v\n", tt) 28 | 29 | var myString string 30 | err = tform.Field(context.Background(), &myString, "set") 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | fmt.Println(myString) 35 | } 36 | 37 | func transformMyData(ctx context.Context, fl mold.FieldLevel) error { 38 | fl.Field().SetString("test") 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /cache.go: -------------------------------------------------------------------------------- 1 | package mold 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | "sync" 7 | "sync/atomic" 8 | ) 9 | 10 | type tagType uint8 11 | 12 | const ( 13 | typeDefault tagType = iota 14 | typeDive 15 | typeKeys 16 | typeEndKeys 17 | ) 18 | 19 | type structCache struct { 20 | lock sync.Mutex 21 | m atomic.Value // map[reflect.Type]*cStruct 22 | } 23 | 24 | func (sc *structCache) Get(key reflect.Type) (c *cStruct, found bool) { 25 | c, found = sc.m.Load().(map[reflect.Type]*cStruct)[key] 26 | return 27 | } 28 | 29 | func (sc *structCache) Set(key reflect.Type, value *cStruct) { 30 | 31 | m := sc.m.Load().(map[reflect.Type]*cStruct) 32 | 33 | nm := make(map[reflect.Type]*cStruct, len(m)+1) 34 | for k, v := range m { 35 | nm[k] = v 36 | } 37 | nm[key] = value 38 | sc.m.Store(nm) 39 | } 40 | 41 | type tagCache struct { 42 | lock sync.Mutex 43 | m atomic.Value // map[string]*cTag 44 | } 45 | 46 | func (tc *tagCache) Get(key string) (c *cTag, found bool) { 47 | c, found = tc.m.Load().(map[string]*cTag)[key] 48 | return 49 | } 50 | 51 | func (tc *tagCache) Set(key string, value *cTag) { 52 | 53 | m := tc.m.Load().(map[string]*cTag) 54 | 55 | nm := make(map[string]*cTag, len(m)+1) 56 | for k, v := range m { 57 | nm[k] = v 58 | } 59 | nm[key] = value 60 | tc.m.Store(nm) 61 | } 62 | 63 | type cStruct struct { 64 | fields []*cField 65 | fn StructLevelFunc 66 | } 67 | 68 | type cField struct { 69 | idx int 70 | cTags *cTag 71 | } 72 | 73 | type cTag struct { 74 | tag string 75 | aliasTag string 76 | actualAliasTag string 77 | hasAlias bool 78 | typeof tagType 79 | hasTag bool 80 | fn Func 81 | keys *cTag 82 | next *cTag 83 | param string 84 | } 85 | 86 | func (t *Transformer) extractStructCache(current reflect.Value) (*cStruct, error) { 87 | t.cCache.lock.Lock() 88 | defer t.cCache.lock.Unlock() 89 | 90 | typ := current.Type() 91 | 92 | // could have been multiple trying to access, but once first is done this ensures struct 93 | // isn't parsed again. 94 | cs, ok := t.cCache.Get(typ) 95 | if ok { 96 | return cs, nil 97 | } 98 | 99 | cs = &cStruct{fields: make([]*cField, 0), fn: t.structLevelFuncs[typ]} 100 | numFields := current.NumField() 101 | 102 | var ctag *cTag 103 | var fld reflect.StructField 104 | var tag string 105 | var err error 106 | 107 | for i := 0; i < numFields; i++ { 108 | 109 | fld = typ.Field(i) 110 | 111 | if !fld.Anonymous && len(fld.PkgPath) > 0 { 112 | continue 113 | } 114 | 115 | tag = fld.Tag.Get(t.tagName) 116 | if tag == ignoreTag { 117 | continue 118 | } 119 | 120 | // NOTE: cannot use shared tag cache, because tags may be equal, but things like alias may be different 121 | // and so only struct level caching can be used instead of combined with Field tag caching 122 | if len(tag) > 0 { 123 | ctag, _, err = t.parseFieldTagsRecursive(tag, fld.Name, "", false) 124 | if err != nil { 125 | return nil, err 126 | } 127 | } else { 128 | // even if field doesn't have validations need cTag for traversing to potential inner/nested 129 | // elements of the field. 130 | ctag = &cTag{typeof: typeDefault} 131 | } 132 | 133 | cs.fields = append(cs.fields, &cField{ 134 | idx: i, 135 | cTags: ctag, 136 | }) 137 | } 138 | 139 | t.cCache.Set(typ, cs) 140 | 141 | return cs, nil 142 | } 143 | 144 | func (t *Transformer) parseFieldTagsRecursive(tag string, fieldName string, alias string, hasAlias bool) (firstCtag *cTag, current *cTag, err error) { 145 | 146 | var tg string 147 | var ok bool 148 | noAlias := len(alias) == 0 149 | tags := strings.Split(tag, tagSeparator) 150 | 151 | for i := 0; i < len(tags); i++ { 152 | 153 | tg = tags[i] 154 | if noAlias { 155 | alias = tg 156 | } 157 | 158 | // check map for alias and process new tags, otherwise process as usual 159 | if tagsVal, found := t.aliases[tg]; found { 160 | if i == 0 { 161 | firstCtag, current, err = t.parseFieldTagsRecursive(tagsVal, fieldName, tg, true) 162 | if err != nil { 163 | return 164 | } 165 | } else { 166 | next, curr, errr := t.parseFieldTagsRecursive(tagsVal, fieldName, tg, true) 167 | if errr != nil { 168 | err = errr 169 | return 170 | } 171 | current.next, current = next, curr 172 | } 173 | continue 174 | } 175 | 176 | var prevTag tagType 177 | 178 | if i == 0 { 179 | current = &cTag{aliasTag: alias, hasAlias: hasAlias, hasTag: true} 180 | firstCtag = current 181 | } else { 182 | prevTag = current.typeof 183 | current.next = &cTag{aliasTag: alias, hasAlias: hasAlias, hasTag: true} 184 | current = current.next 185 | } 186 | 187 | switch tg { 188 | 189 | case diveTag: 190 | current.typeof = typeDive 191 | continue 192 | 193 | case keysTag: 194 | current.typeof = typeKeys 195 | 196 | if i == 0 || prevTag != typeDive { 197 | err = ErrInvalidKeysTag 198 | return 199 | } 200 | 201 | current.typeof = typeKeys 202 | 203 | // need to pass along only keys tag 204 | // need to increment i to skip over the keys tags 205 | b := make([]byte, 0, 64) 206 | 207 | i++ 208 | 209 | for ; i < len(tags); i++ { 210 | 211 | b = append(b, tags[i]...) 212 | b = append(b, ',') 213 | 214 | if tags[i] == endKeysTag { 215 | break 216 | } 217 | } 218 | 219 | if current.keys, _, err = t.parseFieldTagsRecursive(string(b[:len(b)-1]), fieldName, "", false); err != nil { 220 | return 221 | } 222 | continue 223 | 224 | case endKeysTag: 225 | current.typeof = typeEndKeys 226 | 227 | // if there are more in tags then there was no keysTag defined 228 | // and an error should be thrown 229 | if i != len(tags)-1 { 230 | err = ErrUndefinedKeysTag 231 | } 232 | return 233 | 234 | default: 235 | 236 | vals := strings.SplitN(tg, tagKeySeparator, 2) 237 | 238 | if noAlias { 239 | alias = vals[0] 240 | current.aliasTag = alias 241 | } else { 242 | current.actualAliasTag = tg 243 | } 244 | 245 | current.tag = vals[0] 246 | if len(current.tag) == 0 { 247 | err = &ErrInvalidTag{tag: current.tag, field: fieldName} 248 | return 249 | } 250 | 251 | if current.fn, ok = t.transformations[current.tag]; !ok { 252 | err = &ErrUndefinedTag{tag: current.tag, field: fieldName} 253 | return 254 | } 255 | 256 | if len(vals) > 1 { 257 | current.param = strings.Replace(vals[1], utf8HexComma, ",", -1) 258 | } 259 | } 260 | } 261 | return 262 | } 263 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package mold 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | "strings" 8 | ) 9 | 10 | var ( 11 | // ErrInvalidDive describes an invalid dive tag configuration 12 | ErrInvalidDive = errors.New("invalid dive tag configuration") 13 | 14 | // ErrUndefinedKeysTag describes an undefined keys tag when and endkeys tag defined 15 | ErrUndefinedKeysTag = errors.New("'" + endKeysTag + "' tag encountered without a corresponding '" + keysTag + "' tag") 16 | 17 | // ErrInvalidKeysTag describes a misuse of the keys tag 18 | ErrInvalidKeysTag = errors.New("'" + keysTag + "' tag must be immediately preceeded by the '" + diveTag + "' tag") 19 | ) 20 | 21 | // ErrUndefinedTag defines a tag that does not exist 22 | type ErrUndefinedTag struct { 23 | tag string 24 | field string 25 | } 26 | 27 | // Error returns the UndefinedTag error text 28 | func (e *ErrUndefinedTag) Error() string { 29 | return strings.TrimSpace(fmt.Sprintf("unregistered/undefined transformation '%s' found on field %s", e.tag, e.field)) 30 | } 31 | 32 | // ErrInvalidTag defines a bad value for a tag being used 33 | type ErrInvalidTag struct { 34 | tag string 35 | field string 36 | } 37 | 38 | // Error returns the InvalidTag error text 39 | func (e *ErrInvalidTag) Error() string { 40 | return fmt.Sprintf("invalid tag '%s' found on field %s", e.tag, e.field) 41 | } 42 | 43 | // An ErrInvalidTransformValue describes an invalid argument passed to Struct or Var. 44 | // (The argument passed must be a non-nil pointer.) 45 | type ErrInvalidTransformValue struct { 46 | typ reflect.Type 47 | fn string 48 | } 49 | 50 | func (e *ErrInvalidTransformValue) Error() string { 51 | if e.typ == nil { 52 | return fmt.Sprintf("mold: %s(nil)", e.fn) 53 | } 54 | 55 | if e.typ.Kind() != reflect.Ptr { 56 | return fmt.Sprintf("mold: %s(non-pointer %s)", e.fn, e.typ.String()) 57 | } 58 | 59 | return fmt.Sprintf("mold: %s(nil %s)", e.fn, e.typ.String()) 60 | } 61 | 62 | // ErrInvalidTransformation describes an invalid argument passed to 63 | // `Struct` or `Field` 64 | type ErrInvalidTransformation struct { 65 | typ reflect.Type 66 | } 67 | 68 | // Error returns ErrInvalidTransformation message 69 | func (e *ErrInvalidTransformation) Error() string { 70 | return "mold: (nil " + e.typ.String() + ")" 71 | } 72 | -------------------------------------------------------------------------------- /field_level.go: -------------------------------------------------------------------------------- 1 | package mold 2 | 3 | import "reflect" 4 | 5 | // FieldLevel represents the interface for field level modifier function 6 | type FieldLevel interface { 7 | // Transformer represents a subset of the current *Transformer that is executing the current transformation. 8 | Transformer() Transform 9 | 10 | // 11 | // Parent returns the top level parent of the current value return by Field() 12 | // 13 | // This is used primarily for having the ability to nil out pointer type values. 14 | // 15 | // NOTE: that is there are several layers of abstractions eg. interface{} of interface{} of interface{} this 16 | // function returns the first interface{} 17 | // 18 | Parent() reflect.Value 19 | 20 | // Field returns the current field value being modified. 21 | Field() reflect.Value 22 | 23 | // Param returns the param associated wth the given function modifier. 24 | Param() string 25 | } 26 | 27 | var ( 28 | _ FieldLevel = (*fieldLevel)(nil) 29 | ) 30 | 31 | type fieldLevel struct { 32 | transformer *Transformer 33 | parent reflect.Value 34 | current reflect.Value 35 | param string 36 | } 37 | 38 | func (f fieldLevel) Transformer() Transform { 39 | return f.transformer 40 | } 41 | 42 | func (f fieldLevel) Parent() reflect.Value { 43 | return f.parent 44 | } 45 | 46 | func (f fieldLevel) Field() reflect.Value { 47 | return f.current 48 | } 49 | 50 | func (f fieldLevel) Param() string { 51 | return f.param 52 | } 53 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-playground/mold/v4 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/go-playground/assert/v2 v2.2.0 7 | github.com/gosimple/slug v1.15.0 8 | github.com/segmentio/go-camelcase v0.0.0-20160726192923-7085f1e3c734 9 | github.com/segmentio/go-snakecase v1.2.0 10 | github.com/stretchr/testify v1.10.0 11 | golang.org/x/text v0.21.0 12 | ) 13 | 14 | require ( 15 | github.com/davecgh/go-spew v1.1.1 // indirect 16 | github.com/gosimple/unidecode v1.0.1 // indirect 17 | github.com/pmezard/go-difflib v1.0.0 // indirect 18 | gopkg.in/yaml.v3 v3.0.1 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 4 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 5 | github.com/gosimple/slug v1.15.0 h1:wRZHsRrRcs6b0XnxMUBM6WK1U1Vg5B0R7VkIf1Xzobo= 6 | github.com/gosimple/slug v1.15.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= 7 | github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= 8 | github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= 9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 11 | github.com/segmentio/go-camelcase v0.0.0-20160726192923-7085f1e3c734 h1:Cpx2WLIv6fuPvaJAHNhYOgYzk/8RcJXu/8+mOrxf2KM= 12 | github.com/segmentio/go-camelcase v0.0.0-20160726192923-7085f1e3c734/go.mod h1:hqVOMAwu+ekffC3Tvq5N1ljnXRrFKcaSjbCmQ8JgYaI= 13 | github.com/segmentio/go-snakecase v1.2.0 h1:4cTmEjPGi03WmyAHWBjX53viTpBkn/z+4DO++fqYvpw= 14 | github.com/segmentio/go-snakecase v1.2.0/go.mod h1:jk1miR5MS7Na32PZUykG89Arm+1BUSYhuGR6b7+hJto= 15 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 16 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 17 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 18 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 19 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 20 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 21 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 22 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 23 | -------------------------------------------------------------------------------- /modifiers/modifiers.go: -------------------------------------------------------------------------------- 1 | package modifiers 2 | 3 | import ( 4 | "github.com/go-playground/mold/v4" 5 | ) 6 | 7 | // New returns a modifier with defaults registered 8 | func New() *mold.Transformer { 9 | mod := mold.New() 10 | mod.Register("camel", camelCase) 11 | mod.Register("default", defaultValue) 12 | mod.Register("empty", empty) 13 | mod.Register("lcase", toLower) 14 | mod.Register("ltrim", trimLeft) 15 | mod.Register("name", nameCase) 16 | mod.Register("rtrim", trimRight) 17 | mod.Register("set", setValue) 18 | mod.Register("snake", snakeCase) 19 | mod.Register("slug", slugCase) 20 | mod.Register("strip_alpha_unicode", stripAlphaUnicodeCase) 21 | mod.Register("strip_alpha", stripAlphaCase) 22 | mod.Register("strip_num_unicode", stripNumUnicodeCase) 23 | mod.Register("strip_num", stripNumCase) 24 | mod.Register("strip_punctuation", stripPunctuation) 25 | mod.Register("substr", subStr) 26 | mod.Register("title", titleCase) 27 | mod.Register("tprefix", trimPrefix) 28 | mod.Register("trim", trimSpace) 29 | mod.Register("tsuffix", trimSuffix) 30 | mod.Register("ucase", toUpper) 31 | mod.Register("ucfirst", uppercaseFirstCharacterCase) 32 | mod.SetTagName("mod") 33 | return mod 34 | } 35 | -------------------------------------------------------------------------------- /modifiers/multi.go: -------------------------------------------------------------------------------- 1 | package modifiers 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "github.com/go-playground/mold/v4" 11 | ) 12 | 13 | var ( 14 | durationType = reflect.TypeOf(time.Duration(0)) 15 | timeType = reflect.TypeOf(time.Time{}) 16 | ) 17 | 18 | // defaultValue allows setting of a default value IF no value is already present. 19 | func defaultValue(ctx context.Context, fl mold.FieldLevel) error { 20 | if !fl.Field().IsZero() { 21 | return nil 22 | } 23 | return setValue(ctx, fl) 24 | } 25 | 26 | func setValue(_ context.Context, fl mold.FieldLevel) error { 27 | return setValueInner(fl.Field(), fl.Param()) 28 | } 29 | 30 | // setValue allows setting of a specified value 31 | func setValueInner(field reflect.Value, param string) error { 32 | switch field.Kind() { 33 | case reflect.String: 34 | field.SetString(param) 35 | 36 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32: 37 | value, err := strconv.Atoi(param) 38 | if err != nil { 39 | return err 40 | } 41 | field.SetInt(int64(value)) 42 | 43 | case reflect.Int64: 44 | var value int64 45 | 46 | if field.Type() == durationType { 47 | d, err := time.ParseDuration(param) 48 | if err != nil { 49 | return err 50 | } 51 | value = int64(d) 52 | } else { 53 | i, err := strconv.Atoi(param) 54 | if err != nil { 55 | return err 56 | } 57 | value = int64(i) 58 | } 59 | field.SetInt(value) 60 | 61 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 62 | value, err := strconv.Atoi(param) 63 | if err != nil { 64 | return err 65 | } 66 | field.SetUint(uint64(value)) 67 | 68 | case reflect.Float32, reflect.Float64: 69 | value, err := strconv.ParseFloat(param, 64) 70 | if err != nil { 71 | return err 72 | } 73 | field.SetFloat(value) 74 | 75 | case reflect.Bool: 76 | value, err := strconv.ParseBool(param) 77 | if err != nil { 78 | return err 79 | } 80 | field.SetBool(value) 81 | 82 | case reflect.Map: 83 | var n int 84 | var err error 85 | if param != "" { 86 | n, err = strconv.Atoi(param) 87 | if err != nil { 88 | return err 89 | } 90 | } 91 | field.Set(reflect.MakeMapWithSize(field.Type(), n)) 92 | 93 | case reflect.Slice: 94 | var cap int 95 | var err error 96 | if param != "" { 97 | cap, err = strconv.Atoi(param) 98 | if err != nil { 99 | return err 100 | } 101 | } 102 | field.Set(reflect.MakeSlice(field.Type(), 0, cap)) 103 | 104 | case reflect.Struct: 105 | if field.Type() == timeType { 106 | if param != "" { 107 | if strings.ToLower(param) == "utc" { 108 | field.Set(reflect.ValueOf(time.Now().UTC())) 109 | } else { 110 | t, err := time.Parse(time.RFC3339Nano, param) 111 | if err != nil { 112 | return err 113 | } 114 | field.Set(reflect.ValueOf(t)) 115 | } 116 | } else { 117 | field.Set(reflect.ValueOf(time.Now())) 118 | } 119 | } 120 | case reflect.Chan: 121 | var buffer int 122 | var err error 123 | if param != "" { 124 | buffer, err = strconv.Atoi(param) 125 | if err != nil { 126 | return err 127 | } 128 | } 129 | field.Set(reflect.MakeChan(field.Type(), buffer)) 130 | 131 | case reflect.Ptr: 132 | 133 | field.Set(reflect.New(field.Type().Elem())) 134 | return setValueInner(field.Elem(), param) 135 | } 136 | return nil 137 | } 138 | 139 | // empty sets the field to the zero value of the field type 140 | func empty(_ context.Context, fl mold.FieldLevel) error { 141 | zeroValue := reflect.Zero(fl.Field().Type()) 142 | fl.Field().Set(zeroValue) 143 | return nil 144 | } 145 | -------------------------------------------------------------------------------- /modifiers/multi_test.go: -------------------------------------------------------------------------------- 1 | package modifiers 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | . "github.com/go-playground/assert/v2" 9 | ) 10 | 11 | func TestDefaultSetSpecialTypes(t *testing.T) { 12 | conform := New() 13 | 14 | tests := []struct { 15 | name string 16 | field interface{} 17 | tags string 18 | vf func(field interface{}) 19 | expectError bool 20 | }{ 21 | { 22 | name: "default map", 23 | field: (map[string]struct{})(nil), 24 | tags: "default", 25 | vf: func(field interface{}) { 26 | m := field.(map[string]struct{}) 27 | Equal(t, len(m), 0) 28 | }, 29 | }, 30 | { 31 | name: "default map with size", 32 | field: (map[string]struct{})(nil), 33 | tags: "default=5", 34 | vf: func(field interface{}) { 35 | m := field.(map[string]struct{}) 36 | Equal(t, len(m), 0) 37 | }, 38 | }, 39 | { 40 | name: "set map with size", 41 | field: (map[string]struct{})(nil), 42 | tags: "set=5", 43 | vf: func(field interface{}) { 44 | m := field.(map[string]struct{}) 45 | Equal(t, len(m), 0) 46 | }, 47 | }, 48 | { 49 | name: "default slice", 50 | field: ([]string)(nil), 51 | tags: "default", 52 | vf: func(field interface{}) { 53 | m := field.([]string) 54 | Equal(t, len(m), 0) 55 | Equal(t, cap(m), 0) 56 | }, 57 | }, 58 | { 59 | name: "default slice with capacity", 60 | field: ([]string)(nil), 61 | tags: "default=5", 62 | vf: func(field interface{}) { 63 | m := field.([]string) 64 | Equal(t, len(m), 0) 65 | Equal(t, cap(m), 5) 66 | }, 67 | }, 68 | { 69 | name: "set slice", 70 | field: ([]string)(nil), 71 | tags: "set", 72 | vf: func(field interface{}) { 73 | m := field.([]string) 74 | Equal(t, len(m), 0) 75 | Equal(t, cap(m), 0) 76 | }, 77 | }, 78 | { 79 | name: "set slice with capacity", 80 | field: ([]string)(nil), 81 | tags: "set=5", 82 | vf: func(field interface{}) { 83 | m := field.([]string) 84 | Equal(t, len(m), 0) 85 | Equal(t, cap(m), 5) 86 | }, 87 | }, 88 | { 89 | name: "default chan", 90 | field: (chan struct{})(nil), 91 | tags: "default", 92 | vf: func(field interface{}) { 93 | m := field.(chan struct{}) 94 | Equal(t, len(m), 0) 95 | Equal(t, cap(m), 0) 96 | }, 97 | }, 98 | { 99 | name: "default chan with buffer", 100 | field: (chan struct{})(nil), 101 | tags: "default=5", 102 | vf: func(field interface{}) { 103 | m := field.(chan struct{}) 104 | Equal(t, len(m), 0) 105 | Equal(t, cap(m), 5) 106 | }, 107 | }, 108 | { 109 | name: "default time.Time", 110 | field: time.Time{}, 111 | tags: "default", 112 | vf: func(field interface{}) { 113 | m := field.(time.Time) 114 | Equal(t, m.Location(), time.Local) 115 | }, 116 | }, 117 | { 118 | name: "default time.Time utc", 119 | field: time.Time{}, 120 | tags: "default=utc", 121 | vf: func(field interface{}) { 122 | m := field.(time.Time) 123 | Equal(t, m.Location(), time.UTC) 124 | }, 125 | }, 126 | { 127 | name: "default time.Time to value", 128 | field: time.Time{}, 129 | tags: "default=2023-05-28T15:50:31Z", 130 | vf: func(field interface{}) { 131 | m := field.(time.Time) 132 | Equal(t, m.Location(), time.UTC) 133 | 134 | tm, err := time.Parse(time.RFC3339Nano, "2023-05-28T15:50:31Z") 135 | Equal(t, err, nil) 136 | Equal(t, tm.Equal(m), true) 137 | 138 | }, 139 | }, 140 | { 141 | name: "set time.Time", 142 | field: time.Time{}, 143 | tags: "set", 144 | vf: func(field interface{}) { 145 | m := field.(time.Time) 146 | Equal(t, m.Location(), time.Local) 147 | }, 148 | }, 149 | { 150 | name: "set time.Time utc", 151 | field: time.Time{}, 152 | tags: "set=utc", 153 | vf: func(field interface{}) { 154 | m := field.(time.Time) 155 | Equal(t, m.Location(), time.UTC) 156 | }, 157 | }, 158 | { 159 | name: "set time.Time to value", 160 | field: time.Time{}, 161 | tags: "set=2023-05-28T15:50:31Z", 162 | vf: func(field interface{}) { 163 | m := field.(time.Time) 164 | Equal(t, m.Location(), time.UTC) 165 | 166 | tm, err := time.Parse(time.RFC3339Nano, "2023-05-28T15:50:31Z") 167 | Equal(t, err, nil) 168 | Equal(t, tm.Equal(m), true) 169 | 170 | }, 171 | }, 172 | { 173 | name: "set *time.Time to value", 174 | field: (*time.Time)(nil), 175 | tags: "set=2023-05-28T15:50:31Z", 176 | vf: func(field interface{}) { 177 | m := field.(time.Time) 178 | Equal(t, m.Location(), time.UTC) 179 | 180 | tm, err := time.Parse(time.RFC3339Nano, "2023-05-28T15:50:31Z") 181 | Equal(t, err, nil) 182 | Equal(t, tm.Equal(m), true) 183 | 184 | }, 185 | }, 186 | { 187 | name: "default pointer to slice", 188 | field: (*[]string)(nil), 189 | tags: "default", 190 | vf: func(field interface{}) { 191 | m := field.([]string) 192 | Equal(t, len(m), 0) 193 | }, 194 | }, 195 | { 196 | name: "set pointer to slice", 197 | field: (*[]string)(nil), 198 | tags: "set", 199 | vf: func(field interface{}) { 200 | m := field.([]string) 201 | Equal(t, len(m), 0) 202 | }, 203 | }, { 204 | name: "default pointer to int", 205 | field: (*int)(nil), 206 | tags: "default=5", 207 | vf: func(field interface{}) { 208 | m := field.(int) 209 | Equal(t, m, 5) 210 | }, 211 | }, 212 | { 213 | name: "default pointer to string", 214 | field: (*string)(nil), 215 | tags: "default=test", 216 | vf: func(field interface{}) { 217 | m := field.(string) 218 | Equal(t, m, "test") 219 | }, 220 | }, 221 | { 222 | name: "set pointer to string", 223 | field: (*string)(nil), 224 | tags: "set", 225 | vf: func(field interface{}) { 226 | m := field.(string) 227 | Equal(t, m, "") 228 | }, 229 | }, 230 | } 231 | 232 | for _, tc := range tests { 233 | tc := tc 234 | t.Run(tc.name, func(t *testing.T) { 235 | t.Parallel() 236 | err := conform.Field(context.Background(), &tc.field, tc.tags) 237 | if tc.expectError { 238 | NotEqual(t, err, nil) 239 | return 240 | } 241 | Equal(t, err, nil) 242 | tc.vf(tc.field) 243 | }) 244 | } 245 | } 246 | 247 | func TestSet(t *testing.T) { 248 | 249 | type State int 250 | const FINISHED State = 5 251 | 252 | var state State 253 | 254 | conform := New() 255 | 256 | tests := []struct { 257 | name string 258 | field interface{} 259 | tags string 260 | expected interface{} 261 | expectError bool 262 | }{ 263 | { 264 | name: "set State (although enum default value should be the default in practice)", 265 | field: state, 266 | tags: "set=5", 267 | expected: FINISHED, 268 | }, 269 | { 270 | name: "set string", 271 | field: "", 272 | tags: "set=test", 273 | expected: "test", 274 | }, 275 | { 276 | name: "set string", 277 | field: "existing_value", 278 | tags: "set=test", 279 | expected: "test", 280 | }, 281 | { 282 | name: "set int", 283 | field: 0, 284 | tags: "set=3", 285 | expected: 3, 286 | }, 287 | { 288 | name: "set uint", 289 | field: uint(0), 290 | tags: "default=4", 291 | expected: uint(4), 292 | }, 293 | { 294 | name: "set float", 295 | field: float32(0), 296 | tags: "set=5", 297 | expected: float32(5), 298 | }, 299 | { 300 | name: "set bool", 301 | field: false, 302 | tags: "set=true", 303 | expected: true, 304 | }, 305 | { 306 | name: "set time.Duration", 307 | field: time.Duration(0), 308 | tags: "set=1s", 309 | expected: time.Duration(1_000_000_000), 310 | }, 311 | { 312 | name: "bad set time.Duration", 313 | field: time.Duration(0), 314 | tags: "set=rex", 315 | expectError: true, 316 | }, 317 | { 318 | name: "set default int", 319 | field: 0, 320 | tags: "set=abc", 321 | expectError: true, 322 | }, 323 | { 324 | name: "bad set uint", 325 | field: uint(0), 326 | tags: "set=abc", 327 | expectError: true, 328 | }, 329 | { 330 | name: "bad set float", 331 | field: float32(0), 332 | tags: "default=abc", 333 | expectError: true, 334 | }, 335 | { 336 | name: "bad set bool", 337 | field: false, 338 | tags: "default=blue", 339 | expectError: true, 340 | }, 341 | } 342 | 343 | for _, tc := range tests { 344 | tc := tc 345 | t.Run(tc.name, func(t *testing.T) { 346 | t.Parallel() 347 | err := conform.Field(context.Background(), &tc.field, tc.tags) 348 | if tc.expectError { 349 | NotEqual(t, err, nil) 350 | return 351 | } 352 | Equal(t, err, nil) 353 | Equal(t, tc.field, tc.expected) 354 | }) 355 | } 356 | } 357 | 358 | func TestDefault(t *testing.T) { 359 | 360 | type State int 361 | const FINISHED State = 5 362 | 363 | var state State 364 | 365 | conform := New() 366 | 367 | tests := []struct { 368 | name string 369 | field interface{} 370 | tags string 371 | expected interface{} 372 | expectError bool 373 | }{ 374 | { 375 | name: "default State (although enum default value should be the default in practice)", 376 | field: state, 377 | tags: "default=5", 378 | expected: FINISHED, 379 | }, 380 | { 381 | name: "default string", 382 | field: "", 383 | tags: "default=test", 384 | expected: "test", 385 | }, 386 | { 387 | name: "default string", 388 | field: "existing_value", 389 | tags: "default=test", 390 | expected: "existing_value", 391 | }, 392 | { 393 | name: "default int", 394 | field: 0, 395 | tags: "default=3", 396 | expected: 3, 397 | }, 398 | { 399 | name: "default uint", 400 | field: uint(0), 401 | tags: "default=4", 402 | expected: uint(4), 403 | }, 404 | { 405 | name: "default float", 406 | field: float32(0), 407 | tags: "default=5", 408 | expected: float32(5), 409 | }, 410 | { 411 | name: "default bool", 412 | field: false, 413 | tags: "default=true", 414 | expected: true, 415 | }, 416 | { 417 | name: "default time.Duration", 418 | field: time.Duration(0), 419 | tags: "default=1s", 420 | expected: time.Duration(1_000_000_000), 421 | }, 422 | { 423 | name: "bad default time.Duration", 424 | field: time.Duration(0), 425 | tags: "default=rex", 426 | expectError: true, 427 | }, 428 | { 429 | name: "bad default int", 430 | field: 0, 431 | tags: "default=abc", 432 | expectError: true, 433 | }, 434 | { 435 | name: "bad default uint", 436 | field: uint(0), 437 | tags: "default=abc", 438 | expectError: true, 439 | }, 440 | { 441 | name: "bad default float", 442 | field: float32(0), 443 | tags: "default=abc", 444 | expectError: true, 445 | }, 446 | { 447 | name: "bad default bool", 448 | field: false, 449 | tags: "default=blue", 450 | expectError: true, 451 | }, 452 | { 453 | name: "default nil pointer to int", 454 | field: (*int)(nil), 455 | tags: "default=3", 456 | expected: 3, 457 | }, 458 | { 459 | name: "default not nil pointer to int", 460 | field: newPointer(1), 461 | tags: "default=3", 462 | expected: 1, 463 | }, 464 | { 465 | name: "default nil pointer to string", 466 | field: (*string)(nil), 467 | tags: "default=test", 468 | expected: "test", 469 | }, 470 | { 471 | name: "default not nil pointer to string", 472 | field: newPointer("existing_value"), 473 | tags: "default=test", 474 | expected: "existing_value", 475 | }, 476 | { 477 | name: "default nil pointer to bool", 478 | field: (*bool)(nil), 479 | tags: "default=true", 480 | expected: true, 481 | }, 482 | { 483 | name: "default not nil pointer to bool", 484 | field: newPointer(true), 485 | tags: "default=true", 486 | expected: true, 487 | }, 488 | } 489 | 490 | for _, tc := range tests { 491 | tc := tc 492 | t.Run(tc.name, func(t *testing.T) { 493 | t.Parallel() 494 | err := conform.Field(context.Background(), &tc.field, tc.tags) 495 | if tc.expectError { 496 | NotEqual(t, err, nil) 497 | return 498 | } 499 | Equal(t, err, nil) 500 | Equal(t, tc.field, tc.expected) 501 | }) 502 | } 503 | } 504 | 505 | func TestEmpty(t *testing.T) { 506 | 507 | type State int 508 | const FINISHED State = 5 509 | 510 | var state State 511 | 512 | conform := New() 513 | 514 | tests := []struct { 515 | name string 516 | field interface{} 517 | tags string 518 | expected interface{} 519 | expectError bool 520 | }{ 521 | { 522 | name: "empty enum", 523 | field: FINISHED, 524 | tags: "empty", 525 | expected: state, 526 | }, 527 | { 528 | name: "empty string", 529 | field: "test", 530 | tags: "empty", 531 | expected: "", 532 | }, 533 | { 534 | name: "empty int", 535 | field: 10, 536 | tags: "empty", 537 | expected: 0, 538 | }, 539 | { 540 | name: "empty uint", 541 | field: uint(10), 542 | tags: "empty", 543 | expected: uint(0), 544 | }, 545 | { 546 | name: "empty float", 547 | field: float32(10), 548 | tags: "empty", 549 | expected: float32(0), 550 | }, 551 | { 552 | name: "empty bool", 553 | field: true, 554 | tags: "empty", 555 | expected: false, 556 | }, 557 | { 558 | name: "empty time.Duration", 559 | field: time.Duration(10), 560 | tags: "empty", 561 | expected: time.Duration(0), 562 | }, 563 | } 564 | 565 | for _, tc := range tests { 566 | tc := tc 567 | t.Run(tc.name, func(t *testing.T) { 568 | t.Parallel() 569 | err := conform.Field(context.Background(), &tc.field, tc.tags) 570 | if tc.expectError { 571 | NotEqual(t, err, nil) 572 | return 573 | } 574 | Equal(t, err, nil) 575 | Equal(t, tc.field, tc.expected) 576 | }) 577 | } 578 | } 579 | 580 | func newPointer[T any](value T) *T { 581 | return &value 582 | } 583 | -------------------------------------------------------------------------------- /modifiers/string.go: -------------------------------------------------------------------------------- 1 | package modifiers 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "reflect" 7 | "regexp" 8 | "strconv" 9 | "strings" 10 | "unicode" 11 | "unicode/utf8" 12 | 13 | "golang.org/x/text/cases" 14 | "golang.org/x/text/language" 15 | 16 | "github.com/go-playground/mold/v4" 17 | "github.com/gosimple/slug" 18 | "github.com/segmentio/go-camelcase" 19 | "github.com/segmentio/go-snakecase" 20 | ) 21 | 22 | // trimSpace trims extra space from text 23 | func trimSpace(ctx context.Context, fl mold.FieldLevel) error { 24 | switch fl.Field().Kind() { 25 | case reflect.String: 26 | fl.Field().SetString(strings.TrimSpace(fl.Field().String())) 27 | } 28 | return nil 29 | } 30 | 31 | // trimLeft trims extra left hand side of string using provided cutset 32 | func trimLeft(ctx context.Context, fl mold.FieldLevel) error { 33 | switch fl.Field().Kind() { 34 | case reflect.String: 35 | fl.Field().SetString(strings.TrimLeft(fl.Field().String(), fl.Param())) 36 | } 37 | return nil 38 | } 39 | 40 | // trimRight trims extra right hand side of string using provided cutset 41 | func trimRight(ctx context.Context, fl mold.FieldLevel) error { 42 | switch fl.Field().Kind() { 43 | case reflect.String: 44 | fl.Field().SetString(strings.TrimRight(fl.Field().String(), fl.Param())) 45 | } 46 | return nil 47 | } 48 | 49 | // trimPrefix trims the string of a prefix 50 | func trimPrefix(ctx context.Context, fl mold.FieldLevel) error { 51 | switch fl.Field().Kind() { 52 | case reflect.String: 53 | fl.Field().SetString(strings.TrimPrefix(fl.Field().String(), fl.Param())) 54 | } 55 | return nil 56 | } 57 | 58 | // trimSuffix trims the string of a suffix 59 | func trimSuffix(ctx context.Context, fl mold.FieldLevel) error { 60 | switch fl.Field().Kind() { 61 | case reflect.String: 62 | fl.Field().SetString(strings.TrimSuffix(fl.Field().String(), fl.Param())) 63 | } 64 | return nil 65 | } 66 | 67 | // toLower convert string to lower case 68 | func toLower(ctx context.Context, fl mold.FieldLevel) error { 69 | switch fl.Field().Kind() { 70 | case reflect.String: 71 | fl.Field().SetString(strings.ToLower(fl.Field().String())) 72 | } 73 | return nil 74 | } 75 | 76 | // toUpper convert string to upper case 77 | func toUpper(ctx context.Context, fl mold.FieldLevel) error { 78 | switch fl.Field().Kind() { 79 | case reflect.String: 80 | fl.Field().SetString(strings.ToUpper(fl.Field().String())) 81 | } 82 | return nil 83 | } 84 | 85 | // snakeCase converts string to snake case 86 | func snakeCase(ctx context.Context, fl mold.FieldLevel) error { 87 | switch fl.Field().Kind() { 88 | case reflect.String: 89 | fl.Field().SetString(snakecase.Snakecase(fl.Field().String())) 90 | } 91 | return nil 92 | } 93 | 94 | // slug converts string to a slug 95 | func slugCase(ctx context.Context, fl mold.FieldLevel) error { 96 | switch fl.Field().Kind() { 97 | case reflect.String: 98 | fl.Field().SetString(slug.Make(fl.Field().String())) 99 | } 100 | return nil 101 | } 102 | 103 | // titleCase converts string to title case, e.g. "this is a sentence" -> "This Is A Sentence" 104 | func titleCase(ctx context.Context, fl mold.FieldLevel) error { 105 | switch fl.Field().Kind() { 106 | case reflect.String: 107 | fl.Field().SetString(cases.Title(language.Und, cases.NoLower).String(fl.Field().String())) 108 | } 109 | return nil 110 | } 111 | 112 | var namePatterns = []map[string]string{ 113 | {`[^\pL-\s']`: ""}, // cut off everything except [ alpha, hyphen, whitespace, apostrophe] 114 | {`\s{2,}`: " "}, // trim more than two whitespaces to one 115 | {`-{2,}`: "-"}, // trim more than two hyphens to one 116 | {`'{2,}`: "'"}, // trim more than two apostrophes to one 117 | {`( )*-( )*`: "-"}, // trim enclosing whitespaces around hyphen 118 | } 119 | 120 | var nameRegex = regexp.MustCompile(`[\p{L}]([\p{L}|[:space:]\-']*[\p{L}])*`) 121 | 122 | // nameCase Trims, strips numbers and special characters (except dashes and spaces separating names), 123 | // converts multiple spaces and dashes to single characters, title cases multiple names. 124 | // Example: "3493€848Jo-$%£@Ann " -> "Jo-Ann", " ~~ The Dude ~~" -> "The Dude", "**susan**" -> "Susan", 125 | // " hugh fearnley-whittingstall" -> "Hugh Fearnley-Whittingstall" 126 | func nameCase(ctx context.Context, fl mold.FieldLevel) error { 127 | switch fl.Field().Kind() { 128 | case reflect.String: 129 | fl.Field().SetString(cases.Title(language.Und, cases.NoLower).String(nameRegex.FindString(onlyOne(strings.ToLower(fl.Field().String()))))) 130 | } 131 | return nil 132 | } 133 | 134 | func onlyOne(s string) string { 135 | for _, v := range namePatterns { 136 | for f, r := range v { 137 | s = regexp.MustCompile(f).ReplaceAllLiteralString(s, r) 138 | } 139 | } 140 | return s 141 | } 142 | 143 | // uppercaseFirstCharacterCase converts a string so that it has only the first capital letter. Example: "all lower" -> "All lower" 144 | func uppercaseFirstCharacterCase(_ context.Context, fl mold.FieldLevel) error { 145 | switch fl.Field().Kind() { 146 | case reflect.String: 147 | s := fl.Field().String() 148 | if s == "" { 149 | return nil 150 | } 151 | 152 | toRune, size := utf8.DecodeRuneInString(s) 153 | if !unicode.IsLower(toRune) { 154 | return nil 155 | } 156 | buf := &bytes.Buffer{} 157 | buf.WriteRune(unicode.ToUpper(toRune)) 158 | buf.WriteString(s[size:]) 159 | fl.Field().SetString(buf.String()) 160 | } 161 | return nil 162 | } 163 | 164 | var stripNumRegex = regexp.MustCompile("[^0-9]") 165 | 166 | // stripAlphaCase removes all non-numeric characters. Example: "the price is €30,38" -> "3038". Note: The struct field will remain a string. No type conversion takes place. 167 | func stripAlphaCase(_ context.Context, fl mold.FieldLevel) error { 168 | switch fl.Field().Kind() { 169 | case reflect.String: 170 | fl.Field().SetString(stripNumRegex.ReplaceAllLiteralString(fl.Field().String(), "")) 171 | } 172 | return nil 173 | } 174 | 175 | var stripAlphaRegex = regexp.MustCompile("[0-9]") 176 | 177 | // stripNumCase removes all numbers. Example "39472349D34a34v69e8932747" -> "Dave". Note: The struct field will remain a string. No type conversion takes place. 178 | func stripNumCase(_ context.Context, fl mold.FieldLevel) error { 179 | switch fl.Field().Kind() { 180 | case reflect.String: 181 | fl.Field().SetString(stripAlphaRegex.ReplaceAllLiteralString(fl.Field().String(), "")) 182 | } 183 | return nil 184 | } 185 | 186 | var stripNumUnicodeRegex = regexp.MustCompile(`[^\pL]`) 187 | 188 | // stripNumUnicodeCase removes non-alpha unicode characters. Example: "!@£$%^&'()Hello 1234567890 World+[];\" -> "HelloWorld" 189 | func stripNumUnicodeCase(ctx context.Context, fl mold.FieldLevel) error { 190 | switch fl.Field().Kind() { 191 | case reflect.String: 192 | fl.Field().SetString(stripNumUnicodeRegex.ReplaceAllLiteralString(fl.Field().String(), "")) 193 | } 194 | return nil 195 | } 196 | 197 | var stripAlphaUnicode = regexp.MustCompile(`[\pL]`) 198 | 199 | // stripAlphaUnicodeCase removes alpha unicode characters. Example: "Everything's here but the letters!" -> "' !" 200 | func stripAlphaUnicodeCase(ctx context.Context, fl mold.FieldLevel) error { 201 | switch fl.Field().Kind() { 202 | case reflect.String: 203 | fl.Field().SetString(stripAlphaUnicode.ReplaceAllLiteralString(fl.Field().String(), "")) 204 | } 205 | return nil 206 | } 207 | 208 | var stripPunctuationRegex = regexp.MustCompile(`[[:punct:]]`) 209 | 210 | // stripPunctuation removes punctuation. Example: "# M5W-1E6!!!" -> " M5W1E6" 211 | func stripPunctuation(ctx context.Context, fl mold.FieldLevel) error { 212 | switch fl.Field().Kind() { 213 | case reflect.String: 214 | fl.Field().SetString(stripPunctuationRegex.ReplaceAllLiteralString(fl.Field().String(), "")) 215 | } 216 | return nil 217 | } 218 | 219 | // camelCase converts string to camel case 220 | func camelCase(ctx context.Context, fl mold.FieldLevel) error { 221 | switch fl.Field().Kind() { 222 | case reflect.String: 223 | fl.Field().SetString(camelcase.Camelcase(fl.Field().String())) 224 | } 225 | return nil 226 | } 227 | 228 | func subStr(ctx context.Context, fl mold.FieldLevel) error { 229 | switch fl.Field().Kind() { 230 | case reflect.String: 231 | val := fl.Field().String() 232 | params := strings.SplitN(fl.Param(), "-", 2) 233 | if len(params) == 0 || len(params[0]) == 0 { 234 | return nil 235 | } 236 | 237 | start, err := strconv.Atoi(params[0]) 238 | if err != nil { 239 | return err 240 | } 241 | 242 | end := len(val) 243 | if len(params) >= 2 { 244 | end, err = strconv.Atoi(params[1]) 245 | if err != nil { 246 | return err 247 | } 248 | } 249 | 250 | if len(val) < start { 251 | fl.Field().SetString("") 252 | return nil 253 | } 254 | if len(val) < end { 255 | end = len(val) 256 | } 257 | if start > end { 258 | fl.Field().SetString("") 259 | return nil 260 | } 261 | 262 | fl.Field().SetString(val[start:end]) 263 | } 264 | return nil 265 | } 266 | -------------------------------------------------------------------------------- /modifiers/string_test.go: -------------------------------------------------------------------------------- 1 | package modifiers 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "log" 7 | "reflect" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | // NOTES: 15 | // - Run "go test" to run tests 16 | // - Run "gocov test | gocov report" to report on test converage by file 17 | // - Run "gocov test | gocov annotate -" to report on all code and functions, those ,marked with "MISS" were never called 18 | // 19 | // or 20 | // 21 | // -- may be a good idea to change to output path to somewherelike /tmp 22 | // go test -coverprofile cover.out && go tool cover -html=cover.out -o cover.html 23 | // 24 | 25 | func TestMultiple(t *testing.T) { 26 | assert := require.New(t) 27 | conform := New() 28 | s := interface{}("JOEYBLOGGS ") 29 | 30 | err := conform.Field(context.Background(), &s, "trim,lcase") 31 | assert.NoError(err) 32 | assert.Equal("joeybloggs", s) 33 | } 34 | 35 | func TestEnumType(t *testing.T) { 36 | assert := require.New(t) 37 | 38 | type State string 39 | const START State = "start" 40 | state := State("START") 41 | 42 | conform := New() 43 | err := conform.Field(context.Background(), &state, "lcase") 44 | assert.NoError(err) 45 | assert.Equal(START, state) 46 | } 47 | 48 | func TestEmails(t *testing.T) { 49 | conform := New() 50 | 51 | email := " Dean.Karn@gmail.com " 52 | 53 | type Test struct { 54 | Email string `mod:"trim"` 55 | } 56 | 57 | tt := Test{Email: email} 58 | err := conform.Struct(context.Background(), &tt) 59 | if err != nil { 60 | log.Fatal(err) 61 | } 62 | if tt.Email != "Dean.Karn@gmail.com" { 63 | t.Fatalf("Unexpected value '%s'\n", tt.Email) 64 | } 65 | 66 | err = conform.Field(context.Background(), &email, "trim") 67 | if err != nil { 68 | log.Fatal(err) 69 | } 70 | if email != "Dean.Karn@gmail.com" { 71 | t.Fatalf("Unexpected value '%s'\n", tt.Email) 72 | } 73 | 74 | var iface interface{} 75 | err = conform.Field(context.Background(), &iface, "trim") 76 | if err != nil { 77 | log.Fatal(err) 78 | } 79 | if iface != nil { 80 | t.Fatalf("Unexpected value '%v'\n", nil) 81 | } 82 | 83 | iface = " test " 84 | err = conform.Field(context.Background(), &iface, "trim") 85 | if err != nil { 86 | log.Fatal(err) 87 | } 88 | if iface != "test" { 89 | t.Fatalf("Unexpected value '%s'\n", "test") 90 | } 91 | } 92 | 93 | func TestTrimLeft(t *testing.T) { 94 | conform := New() 95 | 96 | s := "#$%_test" 97 | expected := "test" 98 | 99 | type Test struct { 100 | String string `mod:"ltrim=#_$%"` 101 | } 102 | 103 | tt := Test{String: s} 104 | err := conform.Struct(context.Background(), &tt) 105 | if err != nil { 106 | log.Fatal(err) 107 | } 108 | if tt.String != expected { 109 | t.Fatalf("Unexpected value '%s'\n", tt.String) 110 | } 111 | 112 | err = conform.Field(context.Background(), &s, "ltrim=%_$#") 113 | if err != nil { 114 | log.Fatal(err) 115 | } 116 | if s != expected { 117 | t.Fatalf("Unexpected value '%s'\n", s) 118 | } 119 | 120 | var iface interface{} 121 | err = conform.Field(context.Background(), &iface, "ltrim=%_$#") 122 | if err != nil { 123 | log.Fatal(err) 124 | } 125 | if iface != nil { 126 | t.Fatalf("Unexpected value '%v'\n", nil) 127 | } 128 | 129 | iface = s 130 | err = conform.Field(context.Background(), &iface, "ltrim=%_$#") 131 | if err != nil { 132 | log.Fatal(err) 133 | } 134 | if iface != expected { 135 | t.Fatalf("Unexpected value '%v'\n", iface) 136 | } 137 | } 138 | 139 | func TestTrimRight(t *testing.T) { 140 | conform := New() 141 | 142 | s := "test#$%_" 143 | expected := "test" 144 | 145 | type Test struct { 146 | String string `mod:"rtrim=#_$%"` 147 | } 148 | 149 | tt := Test{String: s} 150 | err := conform.Struct(context.Background(), &tt) 151 | if err != nil { 152 | log.Fatal(err) 153 | } 154 | if tt.String != expected { 155 | t.Fatalf("Unexpected value '%s'\n", tt.String) 156 | } 157 | 158 | err = conform.Field(context.Background(), &s, "rtrim=#_$%") 159 | if err != nil { 160 | log.Fatal(err) 161 | } 162 | if s != expected { 163 | t.Fatalf("Unexpected value '%s'\n", s) 164 | } 165 | 166 | var iface interface{} 167 | err = conform.Field(context.Background(), &iface, "rtrim=#_$%") 168 | if err != nil { 169 | log.Fatal(err) 170 | } 171 | if iface != nil { 172 | t.Fatalf("Unexpected value '%v'\n", nil) 173 | } 174 | 175 | iface = s 176 | err = conform.Field(context.Background(), &iface, "rtrim=#_$%") 177 | if err != nil { 178 | log.Fatal(err) 179 | } 180 | if iface != expected { 181 | t.Fatalf("Unexpected value '%v'\n", iface) 182 | } 183 | } 184 | 185 | func TestTrimPrefix(t *testing.T) { 186 | conform := New() 187 | 188 | s := "pre-test" 189 | expected := "test" 190 | 191 | type Test struct { 192 | String string `mod:"tprefix=pre-"` 193 | } 194 | 195 | tt := Test{String: s} 196 | err := conform.Struct(context.Background(), &tt) 197 | if err != nil { 198 | log.Fatal(err) 199 | } 200 | if tt.String != expected { 201 | t.Fatalf("Unexpected value '%s'\n", tt.String) 202 | } 203 | 204 | err = conform.Field(context.Background(), &s, "tprefix=pre-") 205 | if err != nil { 206 | log.Fatal(err) 207 | } 208 | if s != expected { 209 | t.Fatalf("Unexpected value '%s'\n", s) 210 | } 211 | 212 | var iface interface{} 213 | err = conform.Field(context.Background(), &iface, "tprefix=pre-") 214 | if err != nil { 215 | log.Fatal(err) 216 | } 217 | if iface != nil { 218 | t.Fatalf("Unexpected value '%v'\n", nil) 219 | } 220 | 221 | iface = s 222 | err = conform.Field(context.Background(), &iface, "tprefix=pre-") 223 | if err != nil { 224 | log.Fatal(err) 225 | } 226 | if iface != expected { 227 | t.Fatalf("Unexpected value '%v'\n", iface) 228 | } 229 | } 230 | 231 | func TestTrimSuffix(t *testing.T) { 232 | conform := New() 233 | 234 | s := "test-suffix" 235 | expected := "test" 236 | 237 | type Test struct { 238 | String string `mod:"tsuffix=-suffix"` 239 | } 240 | 241 | tt := Test{String: s} 242 | err := conform.Struct(context.Background(), &tt) 243 | if err != nil { 244 | log.Fatal(err) 245 | } 246 | if tt.String != expected { 247 | t.Fatalf("Unexpected value '%s'\n", tt.String) 248 | } 249 | 250 | err = conform.Field(context.Background(), &s, "tsuffix=-suffix") 251 | if err != nil { 252 | log.Fatal(err) 253 | } 254 | if s != expected { 255 | t.Fatalf("Unexpected value '%s'\n", s) 256 | } 257 | 258 | var iface interface{} 259 | err = conform.Field(context.Background(), &iface, "tsuffix=-suffix") 260 | if err != nil { 261 | log.Fatal(err) 262 | } 263 | if iface != nil { 264 | t.Fatalf("Unexpected value '%v'\n", nil) 265 | } 266 | 267 | iface = s 268 | err = conform.Field(context.Background(), &iface, "tsuffix=-suffix") 269 | if err != nil { 270 | log.Fatal(err) 271 | } 272 | if iface != expected { 273 | t.Fatalf("Unexpected value '%v'\n", iface) 274 | } 275 | } 276 | 277 | func TestToLower(t *testing.T) { 278 | conform := New() 279 | 280 | s := "TEST" 281 | expected := "test" 282 | 283 | type Test struct { 284 | String string `mod:"lcase"` 285 | } 286 | 287 | tt := Test{String: s} 288 | err := conform.Struct(context.Background(), &tt) 289 | if err != nil { 290 | log.Fatal(err) 291 | } 292 | if tt.String != expected { 293 | t.Fatalf("Unexpected value '%s'\n", tt.String) 294 | } 295 | 296 | err = conform.Field(context.Background(), &s, "lcase") 297 | if err != nil { 298 | log.Fatal(err) 299 | } 300 | if s != expected { 301 | t.Fatalf("Unexpected value '%s'\n", s) 302 | } 303 | 304 | var iface interface{} 305 | err = conform.Field(context.Background(), &iface, "lcase") 306 | if err != nil { 307 | log.Fatal(err) 308 | } 309 | if iface != nil { 310 | t.Fatalf("Unexpected value '%v'\n", nil) 311 | } 312 | 313 | iface = s 314 | err = conform.Field(context.Background(), &iface, "lcase") 315 | if err != nil { 316 | log.Fatal(err) 317 | } 318 | if iface != expected { 319 | t.Fatalf("Unexpected value '%v'\n", iface) 320 | } 321 | } 322 | 323 | func TestToUpper(t *testing.T) { 324 | conform := New() 325 | 326 | s := "test" 327 | expected := "TEST" 328 | 329 | type Test struct { 330 | String string `mod:"ucase"` 331 | } 332 | 333 | tt := Test{String: s} 334 | err := conform.Struct(context.Background(), &tt) 335 | if err != nil { 336 | log.Fatal(err) 337 | } 338 | if tt.String != expected { 339 | t.Fatalf("Unexpected value '%s'\n", tt.String) 340 | } 341 | 342 | err = conform.Field(context.Background(), &s, "ucase") 343 | if err != nil { 344 | log.Fatal(err) 345 | } 346 | if s != expected { 347 | t.Fatalf("Unexpected value '%s'\n", s) 348 | } 349 | 350 | var iface interface{} 351 | err = conform.Field(context.Background(), &iface, "ucase") 352 | if err != nil { 353 | log.Fatal(err) 354 | } 355 | if iface != nil { 356 | t.Fatalf("Unexpected value '%v'\n", nil) 357 | } 358 | 359 | iface = s 360 | err = conform.Field(context.Background(), &iface, "ucase") 361 | if err != nil { 362 | log.Fatal(err) 363 | } 364 | if iface != expected { 365 | t.Fatalf("Unexpected value '%v'\n", iface) 366 | } 367 | } 368 | 369 | func TestSnakeCase(t *testing.T) { 370 | conform := New() 371 | 372 | s := "ThisIsSNAKEcase" 373 | expected := "this_is_snakecase" 374 | 375 | type Test struct { 376 | String string `mod:"snake"` 377 | } 378 | 379 | tt := Test{String: s} 380 | err := conform.Struct(context.Background(), &tt) 381 | if err != nil { 382 | log.Fatal(err) 383 | } 384 | if tt.String != expected { 385 | t.Fatalf("Unexpected value '%s'\n", tt.String) 386 | } 387 | 388 | err = conform.Field(context.Background(), &s, "snake") 389 | if err != nil { 390 | log.Fatal(err) 391 | } 392 | if s != expected { 393 | t.Fatalf("Unexpected value '%s'\n", s) 394 | } 395 | 396 | var iface interface{} 397 | err = conform.Field(context.Background(), &iface, "snake") 398 | if err != nil { 399 | log.Fatal(err) 400 | } 401 | if iface != nil { 402 | t.Fatalf("Unexpected value '%v'\n", nil) 403 | } 404 | 405 | iface = s 406 | err = conform.Field(context.Background(), &iface, "snake") 407 | if err != nil { 408 | log.Fatal(err) 409 | } 410 | if iface != expected { 411 | t.Fatalf("Unexpected value '%v'\n", iface) 412 | } 413 | } 414 | 415 | func TestTitleCase(t *testing.T) { 416 | conform := New() 417 | 418 | s := "this is a sentence" 419 | expected := "This Is A Sentence" 420 | 421 | type Test struct { 422 | String string `mod:"title"` 423 | } 424 | 425 | tt := Test{String: s} 426 | err := conform.Struct(context.Background(), &tt) 427 | if err != nil { 428 | log.Fatal(err) 429 | } 430 | if tt.String != expected { 431 | t.Fatalf("Unexpected value '%s'\n", tt.String) 432 | } 433 | 434 | err = conform.Field(context.Background(), &s, "title") 435 | if err != nil { 436 | log.Fatal(err) 437 | } 438 | if s != expected { 439 | t.Fatalf("Unexpected value '%s'\n", s) 440 | } 441 | 442 | var iface interface{} 443 | err = conform.Field(context.Background(), &iface, "title") 444 | if err != nil { 445 | log.Fatal(err) 446 | } 447 | if iface != nil { 448 | t.Fatalf("Unexpected value '%v'\n", nil) 449 | } 450 | 451 | iface = s 452 | err = conform.Field(context.Background(), &iface, "title") 453 | if err != nil { 454 | log.Fatal(err) 455 | } 456 | if iface != expected { 457 | t.Fatalf("Unexpected value '%v'\n", iface) 458 | } 459 | } 460 | 461 | func TestSlugCase(t *testing.T) { 462 | conform := New() 463 | 464 | s := "this-is +a SentencE9" 465 | expected := "this-is-a-sentence9" 466 | 467 | type Test struct { 468 | String string `mod:"slug"` 469 | } 470 | 471 | tt := Test{String: s} 472 | err := conform.Struct(context.Background(), &tt) 473 | if err != nil { 474 | log.Fatal(err) 475 | } 476 | if tt.String != expected { 477 | t.Fatalf("Unexpected value '%s'\n", tt.String) 478 | } 479 | 480 | err = conform.Field(context.Background(), &s, "slug") 481 | if err != nil { 482 | log.Fatal(err) 483 | } 484 | if s != expected { 485 | t.Fatalf("Unexpected value '%s'\n", s) 486 | } 487 | 488 | var iface interface{} 489 | err = conform.Field(context.Background(), &iface, "slug") 490 | if err != nil { 491 | log.Fatal(err) 492 | } 493 | if iface != nil { 494 | t.Fatalf("Unexpected value '%v'\n", nil) 495 | } 496 | 497 | iface = s 498 | err = conform.Field(context.Background(), &iface, "slug") 499 | if err != nil { 500 | log.Fatal(err) 501 | } 502 | if iface != expected { 503 | t.Fatalf("Unexpected value '%v'\n", iface) 504 | } 505 | } 506 | 507 | func TestNameCase(t *testing.T) { 508 | conform := New() 509 | 510 | s := "3493€848Jo-$%£@Ann " 511 | expected := "Jo-Ann" 512 | 513 | type Test struct { 514 | String string `mod:"name"` 515 | } 516 | 517 | tt := Test{String: s} 518 | err := conform.Struct(context.Background(), &tt) 519 | if err != nil { 520 | log.Fatal(err) 521 | } 522 | if tt.String != expected { 523 | t.Fatalf("Unexpected value '%s'\n", tt.String) 524 | } 525 | 526 | err = conform.Field(context.Background(), &s, "name") 527 | if err != nil { 528 | log.Fatal(err) 529 | } 530 | if s != expected { 531 | t.Fatalf("Unexpected value '%s'\n", s) 532 | } 533 | 534 | var iface interface{} 535 | err = conform.Field(context.Background(), &iface, "name") 536 | if err != nil { 537 | log.Fatal(err) 538 | } 539 | if iface != nil { 540 | t.Fatalf("Unexpected value '%v'\n", nil) 541 | } 542 | 543 | iface = s 544 | err = conform.Field(context.Background(), &iface, "name") 545 | if err != nil { 546 | log.Fatal(err) 547 | } 548 | if iface != expected { 549 | t.Fatalf("Unexpected value '%v'\n", iface) 550 | } 551 | 552 | s = " ~~ The Dude ~~" 553 | expected = "The Dude" 554 | err = conform.Field(context.Background(), &s, "name") 555 | if err != nil { 556 | log.Fatal(err) 557 | } 558 | if s != expected { 559 | t.Fatalf("Unexpected value '%s'\n", s) 560 | } 561 | 562 | s = "**susan**" 563 | expected = "Susan" 564 | err = conform.Field(context.Background(), &s, "name") 565 | if err != nil { 566 | log.Fatal(err) 567 | } 568 | if s != expected { 569 | t.Fatalf("Unexpected value '%s'\n", s) 570 | } 571 | 572 | s = " hugh fearnley-whittingstall" 573 | expected = "Hugh Fearnley-Whittingstall" 574 | err = conform.Field(context.Background(), &s, "name") 575 | if err != nil { 576 | log.Fatal(err) 577 | } 578 | if s != expected { 579 | t.Fatalf("Unexpected value '%s'\n", s) 580 | } 581 | } 582 | 583 | func TestUCFirstCase(t *testing.T) { 584 | conform := New() 585 | 586 | s := "this is uc first case" 587 | expected := "This is uc first case" 588 | 589 | type Test struct { 590 | String string `mod:"ucfirst"` 591 | } 592 | 593 | tt := Test{String: s} 594 | err := conform.Struct(context.Background(), &tt) 595 | if err != nil { 596 | log.Fatal(err) 597 | } 598 | if tt.String != expected { 599 | t.Fatalf("Unexpected value '%s'\n", tt.String) 600 | } 601 | 602 | err = conform.Field(context.Background(), &s, "ucfirst") 603 | if err != nil { 604 | log.Fatal(err) 605 | } 606 | if s != expected { 607 | t.Fatalf("Unexpected value '%s'\n", s) 608 | } 609 | 610 | var iface interface{} 611 | err = conform.Field(context.Background(), &iface, "ucfirst") 612 | if err != nil { 613 | log.Fatal(err) 614 | } 615 | if iface != nil { 616 | t.Fatalf("Unexpected value '%v'\n", nil) 617 | } 618 | 619 | iface = s 620 | err = conform.Field(context.Background(), &iface, "ucfirst") 621 | if err != nil { 622 | log.Fatal(err) 623 | } 624 | if iface != expected { 625 | t.Fatalf("Unexpected value '%v'\n", iface) 626 | } 627 | 628 | s = "" 629 | expected = "" 630 | err = conform.Field(context.Background(), &s, "ucfirst") 631 | if err != nil { 632 | log.Fatal(err) 633 | } 634 | if s != expected { 635 | t.Fatalf("Unexpected value '%s'\n", s) 636 | } 637 | } 638 | 639 | func TestNumCase(t *testing.T) { 640 | conform := New() 641 | 642 | s := "the price is €30,38" 643 | expected := "3038" 644 | 645 | type Test struct { 646 | String string `mod:"strip_alpha"` 647 | } 648 | 649 | tt := Test{String: s} 650 | err := conform.Struct(context.Background(), &tt) 651 | if err != nil { 652 | log.Fatal(err) 653 | } 654 | if tt.String != expected { 655 | t.Fatalf("Unexpected value '%s'\n", tt.String) 656 | } 657 | 658 | err = conform.Field(context.Background(), &s, "strip_alpha") 659 | if err != nil { 660 | log.Fatal(err) 661 | } 662 | if s != expected { 663 | t.Fatalf("Unexpected value '%s'\n", s) 664 | } 665 | 666 | var iface interface{} 667 | err = conform.Field(context.Background(), &iface, "strip_alpha") 668 | if err != nil { 669 | log.Fatal(err) 670 | } 671 | if iface != nil { 672 | t.Fatalf("Unexpected value '%v'\n", nil) 673 | } 674 | 675 | iface = s 676 | 677 | err = conform.Field(context.Background(), &iface, "strip_alpha") 678 | if err != nil { 679 | log.Fatal(err) 680 | } 681 | if iface != expected { 682 | t.Fatalf("Unexpected value '%v'\n", iface) 683 | } 684 | } 685 | 686 | func TestNotNumCase(t *testing.T) { 687 | conform := New() 688 | 689 | s := "39472349D34a34v69e8932747" 690 | expected := "Dave" 691 | 692 | type Test struct { 693 | String string `mod:"strip_num"` 694 | } 695 | 696 | tt := Test{String: s} 697 | err := conform.Struct(context.Background(), &tt) 698 | if err != nil { 699 | log.Fatal(err) 700 | } 701 | if tt.String != expected { 702 | t.Fatalf("Unexpected value '%s'\n", tt.String) 703 | } 704 | 705 | err = conform.Field(context.Background(), &s, "strip_num") 706 | if err != nil { 707 | log.Fatal(err) 708 | } 709 | if s != expected { 710 | t.Fatalf("Unexpected value '%s'\n", s) 711 | } 712 | 713 | var iface interface{} 714 | err = conform.Field(context.Background(), &iface, "strip_num") 715 | if err != nil { 716 | log.Fatal(err) 717 | } 718 | if iface != nil { 719 | t.Fatalf("Unexpected value '%v'\n", nil) 720 | } 721 | 722 | iface = s 723 | err = conform.Field(context.Background(), &iface, "strip_num") 724 | if err != nil { 725 | log.Fatal(err) 726 | } 727 | if iface != expected { 728 | t.Fatalf("Unexpected value '%v'\n", iface) 729 | } 730 | } 731 | 732 | func TestAlphaCase(t *testing.T) { 733 | conform := New() 734 | 735 | s := "!@£$%^&'()Hello 1234567890 World+[];\\" 736 | expected := "HelloWorld" 737 | 738 | type Test struct { 739 | String string `mod:"strip_num_unicode"` 740 | } 741 | 742 | tt := Test{String: s} 743 | err := conform.Struct(context.Background(), &tt) 744 | if err != nil { 745 | log.Fatal(err) 746 | } 747 | if tt.String != expected { 748 | t.Fatalf("Unexpected value '%s'\n", tt.String) 749 | } 750 | 751 | err = conform.Field(context.Background(), &s, "strip_num_unicode") 752 | if err != nil { 753 | log.Fatal(err) 754 | } 755 | if s != expected { 756 | t.Fatalf("Unexpected value '%s'\n", s) 757 | } 758 | 759 | var iface interface{} 760 | err = conform.Field(context.Background(), &iface, "strip_num_unicode") 761 | if err != nil { 762 | log.Fatal(err) 763 | } 764 | if iface != nil { 765 | t.Fatalf("Unexpected value '%v'\n", nil) 766 | } 767 | 768 | iface = s 769 | err = conform.Field(context.Background(), &iface, "strip_num_unicode") 770 | if err != nil { 771 | log.Fatal(err) 772 | } 773 | if iface != expected { 774 | t.Fatalf("Unexpected value '%v'\n", iface) 775 | } 776 | } 777 | 778 | func TestNotAlphaCase(t *testing.T) { 779 | conform := New() 780 | 781 | s := "Everything's here but the letters!" 782 | expected := "' !" 783 | 784 | type Test struct { 785 | String string `mod:"strip_alpha_unicode"` 786 | } 787 | 788 | tt := Test{String: s} 789 | err := conform.Struct(context.Background(), &tt) 790 | if err != nil { 791 | log.Fatal(err) 792 | } 793 | if tt.String != expected { 794 | t.Fatalf("Unexpected value '%s'\n", tt.String) 795 | } 796 | 797 | err = conform.Field(context.Background(), &s, "strip_alpha_unicode") 798 | if err != nil { 799 | log.Fatal(err) 800 | } 801 | if s != expected { 802 | t.Fatalf("Unexpected value '%s'\n", s) 803 | } 804 | 805 | var iface interface{} 806 | err = conform.Field(context.Background(), &iface, "strip_alpha_unicode") 807 | if err != nil { 808 | log.Fatal(err) 809 | } 810 | if iface != nil { 811 | t.Fatalf("Unexpected value '%v'\n", nil) 812 | } 813 | 814 | iface = s 815 | err = conform.Field(context.Background(), &iface, "strip_alpha_unicode") 816 | if err != nil { 817 | log.Fatal(err) 818 | } 819 | if iface != expected { 820 | t.Fatalf("Unexpected value '%v'\n", iface) 821 | } 822 | } 823 | 824 | func TestPunctuation(t *testing.T) { 825 | conform := New() 826 | 827 | s := "# M5W-1E6!!!" 828 | expected := " M5W1E6" 829 | 830 | type Test struct { 831 | String string `mod:"strip_punctuation"` 832 | } 833 | 834 | tt := Test{String: s} 835 | err := conform.Struct(context.Background(), &tt) 836 | if err != nil { 837 | log.Fatal(err) 838 | } 839 | if tt.String != expected { 840 | t.Fatalf("Unexpected value '%s'\n", tt.String) 841 | } 842 | 843 | err = conform.Field(context.Background(), &s, "strip_punctuation") 844 | if err != nil { 845 | log.Fatal(err) 846 | } 847 | if s != expected { 848 | t.Fatalf("Unexpected value '%s'\n", s) 849 | } 850 | 851 | var iface interface{} 852 | err = conform.Field(context.Background(), &iface, "strip_punctuation") 853 | if err != nil { 854 | log.Fatal(err) 855 | } 856 | if iface != nil { 857 | t.Fatalf("Unexpected value '%v'\n", nil) 858 | } 859 | 860 | iface = s 861 | err = conform.Field(context.Background(), &iface, "strip_punctuation") 862 | if err != nil { 863 | log.Fatal(err) 864 | } 865 | if iface != expected { 866 | t.Fatalf("Unexpected value '%v'\n", iface) 867 | } 868 | } 869 | 870 | func TestCamelCase(t *testing.T) { 871 | conform := New() 872 | 873 | s := "this_is_snakecase" 874 | expected := "thisIsSnakecase" 875 | 876 | type Test struct { 877 | String string `mod:"camel"` 878 | } 879 | 880 | tt := Test{String: s} 881 | err := conform.Struct(context.Background(), &tt) 882 | if err != nil { 883 | log.Fatal(err) 884 | } 885 | if tt.String != expected { 886 | t.Fatalf("Unexpected value '%s'\n", tt.String) 887 | } 888 | 889 | err = conform.Field(context.Background(), &s, "camel") 890 | if err != nil { 891 | log.Fatal(err) 892 | } 893 | if s != expected { 894 | t.Fatalf("Unexpected value '%s'\n", s) 895 | } 896 | 897 | var iface interface{} 898 | err = conform.Field(context.Background(), &iface, "camel") 899 | if err != nil { 900 | log.Fatal(err) 901 | } 902 | if iface != nil { 903 | t.Fatalf("Unexpected value '%v'\n", nil) 904 | } 905 | 906 | iface = s 907 | err = conform.Field(context.Background(), &iface, "camel") 908 | if err != nil { 909 | log.Fatal(err) 910 | } 911 | if iface != expected { 912 | t.Fatalf("Unexpected value '%v'\n", iface) 913 | } 914 | } 915 | 916 | func TestString(t *testing.T) { 917 | assert := require.New(t) 918 | conform := New() 919 | 920 | conform.RegisterInterceptor(func(current reflect.Value) (inner reflect.Value) { 921 | current.FieldByName("Valid").SetBool(true) 922 | return current.FieldByName("String") 923 | }, sql.NullString{}) 924 | 925 | tests := []struct { 926 | name string 927 | field interface{} 928 | tags string 929 | expected interface{} 930 | expectError bool 931 | }{ 932 | { 933 | name: "test Camel Case", 934 | field: "this_is_snakecase", 935 | tags: "camel", 936 | expected: "thisIsSnakecase", 937 | }, 938 | { 939 | name: "test Camel Case struct", 940 | field: struct { 941 | String string `mod:"camel"` 942 | }{String: "this_is_snakecase"}, 943 | tags: "camel", 944 | expected: struct { 945 | String string `mod:"camel"` 946 | }{String: "thisIsSnakecase"}, 947 | }, 948 | { 949 | name: "sql.nullString lcase", 950 | field: sql.NullString{String: "UPPERCASE", Valid: true}, 951 | tags: "lcase", 952 | expected: sql.NullString{String: "uppercase", Valid: true}, 953 | }, 954 | } 955 | 956 | for _, tc := range tests { 957 | tc := tc 958 | t.Run(tc.name, func(t *testing.T) { 959 | t.Parallel() 960 | var err error 961 | 962 | input := reflect.ValueOf(tc.field) 963 | if !input.CanAddr() { 964 | // create NEW addressable pointer to struct and assign the old unadressable one. 965 | // sort fo like: 966 | // 967 | // var newStruct *oldstructdef 968 | // *newStruct = *&oldStruct 969 | // 970 | newVal := reflect.New(input.Type()) 971 | newVal.Elem().Set(input) 972 | tc.field = newVal.Interface() 973 | } 974 | 975 | if reflect.ValueOf(tc.field).Kind() == reflect.Struct && !excludedStructs[reflect.TypeOf(tc.field)] { 976 | err = conform.Struct(context.Background(), &tc.field) 977 | } else { 978 | err = conform.Field(context.Background(), &tc.field, tc.tags) 979 | } 980 | 981 | if tc.expectError { 982 | assert.Error(err) 983 | return 984 | } 985 | assert.NoError(err) 986 | assert.Equal(tc.expected, reflect.Indirect(reflect.ValueOf(tc.field)).Interface()) 987 | }) 988 | } 989 | } 990 | 991 | var ( 992 | excludedStructs = map[reflect.Type]bool{ 993 | reflect.TypeOf(time.Time{}): true, 994 | reflect.TypeOf(sql.NullString{}): true, 995 | } 996 | ) 997 | 998 | func TestSubStr(t *testing.T) { 999 | conform := New() 1000 | 1001 | s := "123" 1002 | expected := "123" 1003 | 1004 | type Test struct { 1005 | String string `mod:"substr=0-3"` 1006 | } 1007 | 1008 | tt := Test{String: s} 1009 | err := conform.Struct(context.Background(), &tt) 1010 | if err != nil { 1011 | log.Fatal(err) 1012 | } 1013 | if tt.String != expected { 1014 | t.Fatalf("Unexpected value '%s'\n", tt.String) 1015 | } 1016 | 1017 | tag := "substr=f-3" 1018 | err = conform.Field(context.Background(), &s, tag) 1019 | if err == nil { 1020 | t.Fatalf("Unexpected value '%s' instead of error for tag %s\n", s, tag) 1021 | } 1022 | tag = "substr=2-f" 1023 | err = conform.Field(context.Background(), &s, tag) 1024 | if err == nil { 1025 | t.Fatalf("Unexpected value '%s' instead of error for tag %s\n", s, tag) 1026 | } 1027 | 1028 | tests := []struct { 1029 | tag string 1030 | expected string 1031 | }{ 1032 | { 1033 | tag: "substr", 1034 | expected: "123", 1035 | }, 1036 | { 1037 | tag: "substr=0-1", 1038 | expected: "1", 1039 | }, 1040 | { 1041 | tag: "substr=0-3", 1042 | expected: "123", 1043 | }, 1044 | { 1045 | tag: "substr=0-2", 1046 | expected: "12", 1047 | }, 1048 | { 1049 | tag: "substr=1-2", 1050 | expected: "2", 1051 | }, 1052 | { 1053 | tag: "substr=3-3", 1054 | expected: "", 1055 | }, 1056 | { 1057 | tag: "substr=4-5", 1058 | expected: "", 1059 | }, 1060 | { 1061 | tag: "substr=2-1", 1062 | expected: "", 1063 | }, 1064 | { 1065 | tag: "substr=2-5", 1066 | expected: "3", 1067 | }, 1068 | { 1069 | tag: "substr=2", 1070 | expected: "3", 1071 | }, 1072 | } 1073 | for _, test := range tests { 1074 | st := s 1075 | 1076 | err = conform.Field(context.Background(), &st, test.tag) 1077 | if err != nil { 1078 | log.Fatal(err) 1079 | } 1080 | if st != test.expected { 1081 | t.Fatalf("Unexpected value '%s' for tag %s\n", st, test.tag) 1082 | } 1083 | 1084 | var iface interface{} 1085 | err = conform.Field(context.Background(), &iface, test.tag) 1086 | if err != nil { 1087 | log.Fatal(err) 1088 | } 1089 | if iface != nil { 1090 | t.Fatalf("Unexpected value '%v'\n", nil) 1091 | } 1092 | 1093 | iface = s 1094 | err = conform.Field(context.Background(), &iface, test.tag) 1095 | if err != nil { 1096 | log.Fatal(err) 1097 | } 1098 | if iface != test.expected { 1099 | t.Fatalf("Unexpected value '%v'\n", iface) 1100 | } 1101 | } 1102 | } 1103 | -------------------------------------------------------------------------------- /mold.go: -------------------------------------------------------------------------------- 1 | package mold 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | var ( 12 | timeType = reflect.TypeOf(time.Time{}) 13 | restrictedAliasErr = "Alias '%s' either contains restricted characters or is the same as a restricted tag needed for normal operation" 14 | restrictedTagErr = "Tag '%s' either contains restricted characters or is the same as a restricted tag needed for normal operation" 15 | ) 16 | 17 | // TODO - ensure StructLevel and Func get passed an interface and not *Transform directly 18 | 19 | // Transform represents a subset of the current *Transformer that is executing the current transformation. 20 | type Transform interface { 21 | Struct(ctx context.Context, v interface{}) error 22 | Field(ctx context.Context, v interface{}, tags string) error 23 | } 24 | 25 | // Func defines a transform function for use. 26 | type Func func(ctx context.Context, fl FieldLevel) error 27 | 28 | // StructLevelFunc accepts all values needed for struct level manipulation. 29 | // 30 | // Why does this exist? For structs for which you may not have access or rights to add tags too, 31 | // from other packages your using. 32 | type StructLevelFunc func(ctx context.Context, sl StructLevel) error 33 | 34 | // InterceptorFunc is a way to intercept custom types to redirect the functions to be applied to an inner typ/value. 35 | // eg. sql.NullString, the manipulation should be done on the inner string. 36 | type InterceptorFunc func(current reflect.Value) (inner reflect.Value) 37 | 38 | // Transformer is the base controlling object which contains 39 | // all necessary information 40 | type Transformer struct { 41 | tagName string 42 | aliases map[string]string 43 | transformations map[string]Func 44 | structLevelFuncs map[reflect.Type]StructLevelFunc 45 | interceptors map[reflect.Type]InterceptorFunc 46 | cCache *structCache 47 | tCache *tagCache 48 | } 49 | 50 | // New creates a new Transform object with default tag name of 'mold' 51 | func New() *Transformer { 52 | tc := new(tagCache) 53 | tc.m.Store(make(map[string]*cTag)) 54 | 55 | sc := new(structCache) 56 | sc.m.Store(make(map[reflect.Type]*cStruct)) 57 | 58 | return &Transformer{ 59 | tagName: "mold", 60 | aliases: make(map[string]string), 61 | transformations: make(map[string]Func), 62 | interceptors: make(map[reflect.Type]InterceptorFunc), 63 | cCache: sc, 64 | tCache: tc, 65 | } 66 | } 67 | 68 | // SetTagName sets the given tag name to be used. 69 | // Default is "trans" 70 | func (t *Transformer) SetTagName(tagName string) { 71 | t.tagName = tagName 72 | } 73 | 74 | // Register adds a transformation with the given tag 75 | // 76 | // NOTES: 77 | // - if the key already exists, the previous transformation function will be replaced. 78 | // - this method is not thread-safe it is intended that these all be registered before hand 79 | func (t *Transformer) Register(tag string, fn Func) { 80 | if len(tag) == 0 { 81 | panic("Function Key cannot be empty") 82 | } 83 | 84 | if fn == nil { 85 | panic("Function cannot be empty") 86 | } 87 | 88 | _, ok := restrictedTags[tag] 89 | 90 | if ok || strings.ContainsAny(tag, restrictedTagChars) { 91 | panic(fmt.Sprintf(restrictedTagErr, tag)) 92 | } 93 | t.transformations[tag] = fn 94 | } 95 | 96 | // RegisterAlias registers a mapping of a single transform tag that 97 | // defines a common or complex set of transformations to simplify adding transforms 98 | // to structs. 99 | // 100 | // NOTE: this function is not thread-safe it is intended that these all be registered before hand 101 | func (t *Transformer) RegisterAlias(alias, tags string) { 102 | if len(alias) == 0 { 103 | panic("Alias cannot be empty") 104 | } 105 | 106 | if len(tags) == 0 { 107 | panic("Aliased tags cannot be empty") 108 | } 109 | 110 | _, ok := restrictedTags[alias] 111 | 112 | if ok || strings.ContainsAny(alias, restrictedTagChars) { 113 | panic(fmt.Sprintf(restrictedAliasErr, alias)) 114 | } 115 | t.aliases[alias] = tags 116 | } 117 | 118 | // RegisterStructLevel registers a StructLevelFunc against a number of types. 119 | // Why does this exist? For structs for which you may not have access or rights to add tags too, 120 | // from other packages your using. 121 | // 122 | // NOTES: 123 | // - this method is not thread-safe it is intended that these all be registered prior to any validation 124 | func (t *Transformer) RegisterStructLevel(fn StructLevelFunc, types ...interface{}) { 125 | if t.structLevelFuncs == nil { 126 | t.structLevelFuncs = make(map[reflect.Type]StructLevelFunc) 127 | } 128 | 129 | for _, typ := range types { 130 | t.structLevelFuncs[reflect.TypeOf(typ)] = fn 131 | } 132 | } 133 | 134 | // RegisterInterceptor registers a new interceptor functions agains one or more types. 135 | // This InterceptorFunc allows one to intercept the incoming to to redirect the application of modifications 136 | // to an inner type/value. 137 | // 138 | // eg. sql.NullString 139 | func (t *Transformer) RegisterInterceptor(fn InterceptorFunc, types ...interface{}) { 140 | for _, typ := range types { 141 | t.interceptors[reflect.TypeOf(typ)] = fn 142 | } 143 | } 144 | 145 | // Struct applies transformations against the provided struct 146 | func (t *Transformer) Struct(ctx context.Context, v interface{}) error { 147 | orig := reflect.ValueOf(v) 148 | 149 | if orig.Kind() != reflect.Ptr || orig.IsNil() { 150 | return &ErrInvalidTransformValue{typ: reflect.TypeOf(v), fn: "Struct"} 151 | } 152 | 153 | val := orig.Elem() 154 | typ := val.Type() 155 | 156 | if val.Kind() != reflect.Struct || val.Type() == timeType { 157 | return &ErrInvalidTransformation{typ: reflect.TypeOf(v)} 158 | } 159 | return t.setByStruct(ctx, orig, val, typ) 160 | } 161 | 162 | func (t *Transformer) setByStruct(ctx context.Context, parent, current reflect.Value, typ reflect.Type) (err error) { 163 | cs, ok := t.cCache.Get(typ) 164 | if !ok { 165 | if cs, err = t.extractStructCache(current); err != nil { 166 | return 167 | } 168 | } 169 | 170 | // run is struct has a corresponding struct level transformation 171 | if cs.fn != nil { 172 | if err = cs.fn(ctx, structLevel{ 173 | transformer: t, 174 | parent: parent, 175 | current: current, 176 | }); err != nil { 177 | return 178 | } 179 | } 180 | 181 | var f *cField 182 | 183 | for i := 0; i < len(cs.fields); i++ { 184 | f = cs.fields[i] 185 | if err = t.setByField(ctx, current.Field(f.idx), f.cTags); err != nil { 186 | return 187 | } 188 | } 189 | return nil 190 | } 191 | 192 | // Field applies the provided transformations against the variable 193 | func (t *Transformer) Field(ctx context.Context, v interface{}, tags string) (err error) { 194 | if len(tags) == 0 || tags == ignoreTag { 195 | return nil 196 | } 197 | 198 | val := reflect.ValueOf(v) 199 | 200 | if val.Kind() != reflect.Ptr || val.IsNil() { 201 | return &ErrInvalidTransformValue{typ: reflect.TypeOf(v), fn: "Field"} 202 | } 203 | val = val.Elem() 204 | 205 | // find cached tag 206 | ctag, ok := t.tCache.Get(tags) 207 | if !ok { 208 | t.tCache.lock.Lock() 209 | 210 | // could have been multiple trying to access, but once first is done this ensures tag 211 | // isn't parsed again. 212 | ctag, ok = t.tCache.Get(tags) 213 | if !ok { 214 | if ctag, _, err = t.parseFieldTagsRecursive(tags, "", "", false); err != nil { 215 | t.tCache.lock.Unlock() 216 | return 217 | } 218 | t.tCache.Set(tags, ctag) 219 | } 220 | t.tCache.lock.Unlock() 221 | } 222 | err = t.setByField(ctx, val, ctag) 223 | return 224 | } 225 | 226 | func (t *Transformer) setByField(ctx context.Context, orig reflect.Value, ct *cTag) (err error) { 227 | current, kind := t.extractType(orig) 228 | 229 | if ct != nil && ct.hasTag { 230 | for ct != nil { 231 | switch ct.typeof { 232 | case typeEndKeys: 233 | return 234 | case typeDive: 235 | ct = ct.next 236 | 237 | switch kind { 238 | case reflect.Slice, reflect.Array: 239 | err = t.setByIterable(ctx, current, ct) 240 | case reflect.Map: 241 | err = t.setByMap(ctx, current, ct) 242 | case reflect.Ptr: 243 | innerKind := current.Type().Elem().Kind() 244 | if innerKind == reflect.Slice || innerKind == reflect.Map { 245 | // is a nil pointer to a slice or map, nothing to do. 246 | return nil 247 | } 248 | // not a valid use of the dive tag 249 | fallthrough 250 | default: 251 | err = ErrInvalidDive 252 | } 253 | return 254 | 255 | default: 256 | if !current.CanAddr() { 257 | newVal := reflect.New(current.Type()).Elem() 258 | newVal.Set(current) 259 | if err = ct.fn(ctx, fieldLevel{ 260 | transformer: t, 261 | parent: orig, 262 | current: newVal, 263 | param: ct.param, 264 | }); err != nil { 265 | return 266 | } 267 | orig.Set(reflect.Indirect(newVal)) 268 | current, kind = t.extractType(orig) 269 | } else { 270 | if err = ct.fn(ctx, fieldLevel{ 271 | transformer: t, 272 | parent: orig, 273 | current: current, 274 | param: ct.param, 275 | }); err != nil { 276 | return 277 | } 278 | // value could have been changed or reassigned 279 | current, kind = t.extractType(current) 280 | } 281 | ct = ct.next 282 | } 283 | } 284 | } 285 | 286 | // need to do this again because one of the previous 287 | // sets could have set a struct value, where it was a 288 | // nil pointer before 289 | orig2 := current 290 | current, kind = t.extractType(current) 291 | 292 | if kind == reflect.Struct { 293 | typ := current.Type() 294 | if typ == timeType { 295 | return 296 | } 297 | 298 | if !current.CanAddr() { 299 | newVal := reflect.New(typ).Elem() 300 | newVal.Set(current) 301 | 302 | if err = t.setByStruct(ctx, orig, newVal, typ); err != nil { 303 | return 304 | } 305 | orig.Set(reflect.Indirect(newVal)) 306 | return 307 | } 308 | err = t.setByStruct(ctx, orig2, current, typ) 309 | } 310 | return 311 | } 312 | 313 | func (t *Transformer) setByIterable(ctx context.Context, current reflect.Value, ct *cTag) (err error) { 314 | for i := 0; i < current.Len(); i++ { 315 | if err = t.setByField(ctx, current.Index(i), ct); err != nil { 316 | return 317 | } 318 | } 319 | return 320 | } 321 | 322 | func (t *Transformer) setByMap(ctx context.Context, current reflect.Value, ct *cTag) error { 323 | for _, key := range current.MapKeys() { 324 | newVal := reflect.New(current.Type().Elem()).Elem() 325 | newVal.Set(current.MapIndex(key)) 326 | 327 | if ct != nil && ct.typeof == typeKeys && ct.keys != nil { 328 | // remove current map key as we may be changing it 329 | // and re-add to the map afterwards 330 | current.SetMapIndex(key, reflect.Value{}) 331 | 332 | newKey := reflect.New(current.Type().Key()).Elem() 333 | newKey.Set(key) 334 | key = newKey 335 | 336 | // handle map key 337 | if err := t.setByField(ctx, key, ct.keys); err != nil { 338 | return err 339 | } 340 | 341 | // can be nil when just keys being validated 342 | if ct.next != nil { 343 | if err := t.setByField(ctx, newVal, ct.next); err != nil { 344 | return err 345 | } 346 | } 347 | } else { 348 | if err := t.setByField(ctx, newVal, ct); err != nil { 349 | return err 350 | } 351 | } 352 | current.SetMapIndex(key, newVal) 353 | } 354 | 355 | return nil 356 | } 357 | -------------------------------------------------------------------------------- /mold_test.go: -------------------------------------------------------------------------------- 1 | package mold 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "reflect" 7 | "strings" 8 | "testing" 9 | "time" 10 | 11 | . "github.com/go-playground/assert/v2" 12 | ) 13 | 14 | // NOTES: 15 | // - Run "go test" to run tests 16 | // - Run "gocov test | gocov report" to report on test converage by file 17 | // - Run "gocov test | gocov annotate -" to report on all code and functions, those ,marked with "MISS" were never called 18 | // 19 | // or 20 | // 21 | // -- may be a good idea to change to output path to somewherelike /tmp 22 | // go test -coverprofile cover.out && go tool cover -html=cover.out -o cover.html 23 | // 24 | 25 | func TestBadValues(t *testing.T) { 26 | tform := New() 27 | tform.Register("blah", func(ctx context.Context, fl FieldLevel) error { return nil }) 28 | 29 | type Test struct { 30 | Ignore string `mold:"-"` 31 | String string `mold:"blah,,blah"` 32 | } 33 | 34 | var tt Test 35 | 36 | err := tform.Struct(context.Background(), &tt) 37 | NotEqual(t, err, nil) 38 | Equal(t, err.Error(), "invalid tag '' found on field String") 39 | 40 | err = tform.Struct(context.Background(), tt) 41 | NotEqual(t, err, nil) 42 | Equal(t, err.Error(), "mold: Struct(non-pointer mold.Test)") 43 | 44 | err = tform.Struct(context.Background(), nil) 45 | NotEqual(t, err, nil) 46 | Equal(t, err.Error(), "mold: Struct(nil)") 47 | 48 | var i int 49 | err = tform.Struct(context.Background(), &i) 50 | NotEqual(t, err, nil) 51 | Equal(t, err.Error(), "mold: (nil *int)") 52 | 53 | var iface interface{} 54 | err = tform.Struct(context.Background(), iface) 55 | NotEqual(t, err, nil) 56 | Equal(t, err.Error(), "mold: Struct(nil)") 57 | 58 | iface = nil 59 | err = tform.Struct(context.Background(), &iface) 60 | NotEqual(t, err, nil) 61 | Equal(t, err.Error(), "mold: (nil *interface {})") 62 | 63 | var tst *Test 64 | err = tform.Struct(context.Background(), tst) 65 | NotEqual(t, err, nil) 66 | Equal(t, err.Error(), "mold: Struct(nil *mold.Test)") 67 | 68 | var tm *time.Time 69 | err = tform.Field(context.Background(), tm, "blah") 70 | NotEqual(t, err, nil) 71 | Equal(t, err.Error(), "mold: Field(nil *time.Time)") 72 | 73 | PanicMatches(t, func() { tform.Register("", nil) }, "Function Key cannot be empty") 74 | PanicMatches(t, func() { tform.Register("test", nil) }, "Function cannot be empty") 75 | PanicMatches(t, func() { 76 | tform.Register(",", func(ctx context.Context, fl FieldLevel) error { return nil }) 77 | }, "Tag ',' either contains restricted characters or is the same as a restricted tag needed for normal operation") 78 | 79 | PanicMatches(t, func() { tform.RegisterAlias("", "") }, "Alias cannot be empty") 80 | PanicMatches(t, func() { tform.RegisterAlias("test", "") }, "Aliased tags cannot be empty") 81 | PanicMatches(t, func() { tform.RegisterAlias(",", "test") }, "Alias ',' either contains restricted characters or is the same as a restricted tag needed for normal operation") 82 | } 83 | 84 | func TestBasicTransform(t *testing.T) { 85 | 86 | type Test struct { 87 | String string `r:"repl"` 88 | } 89 | 90 | var tt Test 91 | 92 | set := New() 93 | set.SetTagName("r") 94 | set.Register("repl", func(ctx context.Context, fl FieldLevel) error { 95 | fl.Field().SetString("test") 96 | return nil 97 | }) 98 | 99 | val := reflect.ValueOf(tt) 100 | // trigger a wait in struct parsing 101 | for i := 0; i < 3; i++ { 102 | _, err := set.extractStructCache(val) 103 | Equal(t, err, nil) 104 | } 105 | err := set.Struct(context.Background(), &tt) 106 | Equal(t, err, nil) 107 | Equal(t, tt.String, "test") 108 | 109 | type Test2 struct { 110 | Test Test 111 | String string `r:"repl"` 112 | } 113 | 114 | var tt2 Test2 115 | 116 | err = set.Struct(context.Background(), &tt2) 117 | Equal(t, err, nil) 118 | Equal(t, tt2.Test.String, "test") 119 | Equal(t, tt2.String, "test") 120 | 121 | type Test3 struct { 122 | Test 123 | String string `r:"repl"` 124 | } 125 | 126 | var tt3 Test3 127 | 128 | err = set.Struct(context.Background(), &tt3) 129 | Equal(t, err, nil) 130 | Equal(t, tt3.Test.String, "test") 131 | Equal(t, tt3.String, "test") 132 | 133 | type Test4 struct { 134 | Test *Test 135 | String string `r:"repl"` 136 | } 137 | 138 | var tt4 Test4 139 | 140 | err = set.Struct(context.Background(), &tt4) 141 | Equal(t, err, nil) 142 | Equal(t, tt4.Test, nil) 143 | Equal(t, tt4.String, "test") 144 | 145 | tt5 := Test4{Test: &Test{}} 146 | 147 | err = set.Struct(context.Background(), &tt5) 148 | Equal(t, err, nil) 149 | Equal(t, tt5.Test.String, "test") 150 | Equal(t, tt5.String, "test") 151 | 152 | type Test6 struct { 153 | Test *Test `r:"default"` 154 | String string `r:"repl"` 155 | } 156 | 157 | var tt6 Test6 158 | 159 | set.Register("default", func(ctx context.Context, fl FieldLevel) error { 160 | fl.Field().Set(reflect.New(fl.Field().Type().Elem())) 161 | return nil 162 | }) 163 | err = set.Struct(context.Background(), &tt6) 164 | Equal(t, err, nil) 165 | NotEqual(t, tt6.Test, nil) 166 | Equal(t, tt6.Test.String, "test") 167 | Equal(t, tt6.String, "test") 168 | 169 | tt6.String = "BAD" 170 | var tString string 171 | 172 | // wil invoke one processing and one waiting 173 | go func() { 174 | err := set.Field(context.Background(), &tString, "repl") 175 | Equal(t, err, nil) 176 | }() 177 | err = set.Field(context.Background(), &tt6.String, "repl") 178 | Equal(t, err, nil) 179 | Equal(t, tt6.String, "test") 180 | 181 | err = set.Field(context.Background(), &tt6.String, "") 182 | Equal(t, err, nil) 183 | 184 | err = set.Field(context.Background(), &tt6.String, "-") 185 | Equal(t, err, nil) 186 | 187 | err = set.Field(context.Background(), tt6.String, "test") 188 | NotEqual(t, err, nil) 189 | Equal(t, err.Error(), "mold: Field(non-pointer string)") 190 | 191 | err = set.Field(context.Background(), nil, "test") 192 | NotEqual(t, err, nil) 193 | Equal(t, err.Error(), "mold: Field(nil)") 194 | 195 | var iface interface{} 196 | err = set.Field(context.Background(), iface, "test") 197 | NotEqual(t, err, nil) 198 | Equal(t, err.Error(), "mold: Field(nil)") 199 | 200 | done := make(chan struct{}) 201 | go func() { 202 | err := set.Field(context.Background(), &tString, "nonexistant") 203 | NotEqual(t, err, nil) 204 | close(done) 205 | }() 206 | 207 | err = set.Field(context.Background(), &tt6.String, "nonexistant") 208 | NotEqual(t, err, nil) 209 | Equal(t, err.Error(), "unregistered/undefined transformation 'nonexistant' found on field") 210 | 211 | <-done 212 | set.Register("dummy", func(ctx context.Context, fl FieldLevel) error { return nil }) 213 | err = set.Field(context.Background(), &tt6.String, "dummy") 214 | Equal(t, err, nil) 215 | } 216 | 217 | func TestAlias(t *testing.T) { 218 | 219 | type Test struct { 220 | String string `r:"repl,repl2"` 221 | } 222 | 223 | var tt Test 224 | 225 | set := New() 226 | set.SetTagName("r") 227 | set.Register("repl", func(ctx context.Context, fl FieldLevel) error { 228 | fl.Field().SetString("test") 229 | return nil 230 | }) 231 | set.Register("repl2", func(ctx context.Context, fl FieldLevel) error { 232 | fl.Field().SetString("test2") 233 | return nil 234 | }) 235 | 236 | err := set.Struct(context.Background(), &tt) 237 | Equal(t, err, nil) 238 | Equal(t, tt.String, "test2") 239 | 240 | set.RegisterAlias("rep", "repl,repl2") 241 | set.RegisterAlias("bad", "repl,,repl2") 242 | type Test2 struct { 243 | String string `r:"rep"` 244 | } 245 | 246 | var tt2 Test2 247 | 248 | err = set.Struct(context.Background(), &tt2) 249 | Equal(t, err, nil) 250 | Equal(t, tt.String, "test2") 251 | 252 | var s string 253 | err = set.Field(context.Background(), &s, "bad") 254 | NotEqual(t, err, nil) 255 | 256 | // var s string 257 | err = set.Field(context.Background(), &s, "repl,rep,bad") 258 | NotEqual(t, err, nil) 259 | } 260 | 261 | func TestArray(t *testing.T) { 262 | type Test struct { 263 | Arr []string `s:"defaultArr,dive,defaultStr"` 264 | } 265 | 266 | set := New() 267 | set.SetTagName("s") 268 | set.Register("defaultArr", func(ctx context.Context, fl FieldLevel) error { 269 | if HasValue(fl.Field()) { 270 | return nil 271 | } 272 | fl.Field().Set(reflect.MakeSlice(fl.Field().Type(), 2, 2)) 273 | return nil 274 | }) 275 | set.Register("defaultStr", func(ctx context.Context, fl FieldLevel) error { 276 | if fl.Field().String() == "ok" { 277 | return errors.New("ALREADY OK") 278 | } 279 | fl.Field().SetString("default") 280 | return nil 281 | }) 282 | 283 | var tt Test 284 | 285 | err := set.Struct(context.Background(), &tt) 286 | Equal(t, err, nil) 287 | Equal(t, len(tt.Arr), 2) 288 | Equal(t, tt.Arr[0], "default") 289 | Equal(t, tt.Arr[1], "default") 290 | 291 | tt2 := Test{ 292 | Arr: make([]string, 1), 293 | } 294 | 295 | err = set.Struct(context.Background(), &tt2) 296 | Equal(t, err, nil) 297 | Equal(t, len(tt2.Arr), 1) 298 | Equal(t, tt2.Arr[0], "default") 299 | 300 | tt3 := Test{ 301 | Arr: []string{"ok"}, 302 | } 303 | 304 | err = set.Struct(context.Background(), &tt3) 305 | NotEqual(t, err, nil) 306 | Equal(t, err.Error(), "ALREADY OK") 307 | } 308 | 309 | func TestMap(t *testing.T) { 310 | type Test struct { 311 | Map map[string]string `s:"defaultMap,dive,defaultStr"` 312 | } 313 | 314 | set := New() 315 | set.SetTagName("s") 316 | set.Register("defaultMap", func(ctx context.Context, fl FieldLevel) error { 317 | if HasValue(fl.Field()) { 318 | return nil 319 | } 320 | fl.Field().Set(reflect.MakeMap(fl.Field().Type())) 321 | return nil 322 | }) 323 | set.Register("defaultStr", func(ctx context.Context, fl FieldLevel) error { 324 | if fl.Field().String() == "ok" { 325 | return errors.New("ALREADY OK") 326 | } 327 | fl.Field().SetString("default") 328 | return nil 329 | }) 330 | 331 | var tt Test 332 | 333 | err := set.Struct(context.Background(), &tt) 334 | Equal(t, err, nil) 335 | Equal(t, len(tt.Map), 0) 336 | 337 | tt2 := Test{ 338 | Map: map[string]string{"key": ""}, 339 | } 340 | 341 | err = set.Struct(context.Background(), &tt2) 342 | Equal(t, err, nil) 343 | Equal(t, len(tt2.Map), 1) 344 | Equal(t, tt2.Map["key"], "default") 345 | 346 | tt3 := Test{ 347 | Map: map[string]string{"key": "ok"}, 348 | } 349 | 350 | err = set.Struct(context.Background(), &tt3) 351 | NotEqual(t, err, nil) 352 | Equal(t, err.Error(), "ALREADY OK") 353 | } 354 | 355 | func TestInterface(t *testing.T) { 356 | type Test struct { 357 | Iface interface{} `s:"default"` 358 | } 359 | 360 | type Inner struct { 361 | STR string 362 | String string `s:"defaultStr"` 363 | } 364 | 365 | type Test2 struct { 366 | Iface interface{} `s:"default2"` 367 | } 368 | 369 | type Inner2 struct { 370 | String string `s:"error"` 371 | } 372 | 373 | set := New() 374 | set.SetTagName("s") 375 | set.Register("default", func(ctx context.Context, fl FieldLevel) error { 376 | fl.Field().Set(reflect.ValueOf(Inner{STR: "test"})) 377 | return nil 378 | }) 379 | set.Register("default2", func(ctx context.Context, fl FieldLevel) error { 380 | fl.Field().Set(reflect.ValueOf(Inner2{})) 381 | return nil 382 | }) 383 | set.Register("defaultStr", func(ctx context.Context, fl FieldLevel) error { 384 | if HasValue(fl.Field()) && fl.Field().String() == "ok" { 385 | return errors.New("ALREADY OK") 386 | } 387 | fl.Field().Set(reflect.ValueOf("default")) 388 | return nil 389 | }) 390 | set.Register("error", func(ctx context.Context, fl FieldLevel) error { 391 | return errors.New("BAD VALUE") 392 | }) 393 | 394 | var tt Test 395 | 396 | err := set.Struct(context.Background(), &tt) 397 | Equal(t, err, nil) 398 | NotEqual(t, tt.Iface, nil) 399 | 400 | inner, ok := tt.Iface.(Inner) 401 | Equal(t, ok, true) 402 | Equal(t, inner.String, "default") 403 | Equal(t, inner.STR, "test") 404 | 405 | var tt2 Test2 406 | 407 | err = set.Struct(context.Background(), &tt2) 408 | NotEqual(t, err, nil) 409 | 410 | type Test3 struct { 411 | Iface interface{} `s:"defaultStr"` 412 | } 413 | 414 | var tt3 Test3 415 | tt3.Iface = "String" 416 | err = set.Struct(context.Background(), &tt3) 417 | Equal(t, err, nil) 418 | Equal(t, tt3.Iface.(string), "default") 419 | 420 | type Test4 struct { 421 | Iface interface{} `s:"defaultStr,defaultStr"` 422 | } 423 | 424 | var tt4 Test4 425 | tt4.Iface = nil 426 | err = set.Struct(context.Background(), &tt4) 427 | Equal(t, err, nil) 428 | Equal(t, tt4.Iface.(string), "default") 429 | 430 | type Test5 struct { 431 | Iface interface{} `s:"defaultStr,error"` 432 | } 433 | 434 | var tt5 Test5 435 | tt5.Iface = "String" 436 | err = set.Struct(context.Background(), &tt5) 437 | NotEqual(t, err, nil) 438 | } 439 | 440 | func TestInterfacePtr(t *testing.T) { 441 | type Test struct { 442 | Iface interface{} `s:"default"` 443 | } 444 | 445 | type Inner struct { 446 | String string `s:"defaultStr"` 447 | } 448 | 449 | set := New() 450 | set.SetTagName("s") 451 | set.Register("default", func(ctx context.Context, fl FieldLevel) error { 452 | fl.Field().Set(reflect.ValueOf(new(Inner))) 453 | return nil 454 | }) 455 | set.Register("defaultStr", func(ctx context.Context, fl FieldLevel) error { 456 | if fl.Field().String() == "ok" { 457 | return errors.New("ALREADY OK") 458 | } 459 | fl.Field().SetString("default") 460 | return nil 461 | }) 462 | 463 | var tt Test 464 | 465 | err := set.Struct(context.Background(), &tt) 466 | Equal(t, err, nil) 467 | NotEqual(t, tt.Iface, nil) 468 | 469 | inner, ok := tt.Iface.(*Inner) 470 | Equal(t, ok, true) 471 | Equal(t, inner.String, "default") 472 | 473 | type Test2 struct { 474 | Iface interface{} 475 | } 476 | 477 | var tt2 Test2 478 | tt2.Iface = Inner{} 479 | err = set.Struct(context.Background(), &tt2) 480 | Equal(t, err, nil) 481 | } 482 | 483 | func TestStructLevel(t *testing.T) { 484 | type Test struct { 485 | String string 486 | } 487 | 488 | set := New() 489 | set.RegisterStructLevel(func(ctx context.Context, sl StructLevel) error { 490 | s := sl.Struct().Interface().(Test) 491 | if s.String == "error" { 492 | return errors.New("BAD VALUE") 493 | } 494 | s.String = "test" 495 | sl.Struct().Set(reflect.ValueOf(s)) 496 | return nil 497 | }, Test{}) 498 | 499 | var tt Test 500 | err := set.Struct(context.Background(), &tt) 501 | Equal(t, err, nil) 502 | Equal(t, tt.String, "test") 503 | 504 | tt.String = "error" 505 | err = set.Struct(context.Background(), &tt) 506 | NotEqual(t, err, nil) 507 | } 508 | 509 | func TestTimeType(t *testing.T) { 510 | 511 | var tt time.Time 512 | 513 | set := New() 514 | set.Register("default", func(ctx context.Context, fl FieldLevel) error { 515 | fl.Field().Set(reflect.ValueOf(time.Now())) 516 | return nil 517 | }) 518 | 519 | err := set.Field(context.Background(), &tt, "default") 520 | Equal(t, err, nil) 521 | 522 | err = set.Field(context.Background(), &tt, "default,dive") 523 | NotEqual(t, err, nil) 524 | Equal(t, errors.Is(err, ErrInvalidDive), true) 525 | } 526 | 527 | func TestParam(t *testing.T) { 528 | 529 | type Test struct { 530 | String string `r:"ltrim=#$_"` 531 | } 532 | 533 | set := New() 534 | set.SetTagName("r") 535 | set.Register("ltrim", func(ctx context.Context, fl FieldLevel) error { 536 | fl.Field().SetString(strings.TrimLeft(fl.Field().String(), fl.Param())) 537 | return nil 538 | }) 539 | 540 | tt := Test{String: "_test"} 541 | 542 | err := set.Struct(context.Background(), &tt) 543 | Equal(t, err, nil) 544 | Equal(t, tt.String, "test") 545 | } 546 | 547 | func TestDiveKeys(t *testing.T) { 548 | 549 | type Test struct { 550 | Map map[string]string `s:"dive,keys,default,endkeys,default"` 551 | } 552 | 553 | set := New() 554 | set.SetTagName("s") 555 | set.Register("default", func(ctx context.Context, fl FieldLevel) error { 556 | fl.Field().Set(reflect.ValueOf("after")) 557 | return nil 558 | }) 559 | set.Register("err", func(ctx context.Context, fl FieldLevel) error { 560 | return errors.New("err") 561 | }) 562 | 563 | test := Test{ 564 | Map: map[string]string{ 565 | "b4": "b4", 566 | }, 567 | } 568 | 569 | err := set.Struct(context.Background(), &test) 570 | Equal(t, err, nil) 571 | 572 | val := test.Map["after"] 573 | Equal(t, val, "after") 574 | 575 | m := map[string]string{ 576 | "b4": "b4", 577 | } 578 | 579 | err = set.Field(context.Background(), &m, "dive,keys,default,endkeys,default") 580 | Equal(t, err, nil) 581 | 582 | val = m["after"] 583 | Equal(t, val, "after") 584 | 585 | err = set.Field(context.Background(), &m, "keys,endkeys,default") 586 | Equal(t, err, ErrInvalidKeysTag) 587 | 588 | err = set.Field(context.Background(), &m, "dive,endkeys,default") 589 | Equal(t, err, ErrUndefinedKeysTag) 590 | 591 | err = set.Field(context.Background(), &m, "dive,keys,undefinedtag") 592 | Equal(t, err, ErrUndefinedTag{tag: "undefinedtag"}) 593 | 594 | err = set.Field(context.Background(), &m, "dive,keys,err,endkeys") 595 | NotEqual(t, err, nil) 596 | 597 | m = map[string]string{ 598 | "b4": "b4", 599 | } 600 | err = set.Field(context.Background(), &m, "dive,keys,default,endkeys,err") 601 | NotEqual(t, err, nil) 602 | } 603 | 604 | func TestStructArray(t *testing.T) { 605 | type InnerStruct struct { 606 | String string `s:"defaultStr"` 607 | } 608 | 609 | type Test struct { 610 | Inner InnerStruct 611 | Arr []InnerStruct `s:"defaultArr"` 612 | ArrDive []InnerStruct `s:"defaultArr,dive"` 613 | ArrNoTag []InnerStruct 614 | } 615 | 616 | set := New() 617 | set.SetTagName("s") 618 | set.Register("defaultArr", func(ctx context.Context, fl FieldLevel) error { 619 | if HasValue(fl.Field()) { 620 | return nil 621 | } 622 | fl.Field().Set(reflect.MakeSlice(fl.Field().Type(), 2, 2)) 623 | return nil 624 | }) 625 | set.Register("defaultStr", func(ctx context.Context, fl FieldLevel) error { 626 | if fl.Field().String() == "ok" { 627 | return errors.New("ALREADY OK") 628 | } 629 | fl.Field().SetString("default") 630 | return nil 631 | }) 632 | 633 | var tt Test 634 | 635 | err := set.Struct(context.Background(), &tt) 636 | Equal(t, err, nil) 637 | Equal(t, len(tt.Arr), 2) 638 | Equal(t, len(tt.ArrDive), 2) 639 | Equal(t, tt.Arr[0].String, "") 640 | Equal(t, tt.Arr[1].String, "") 641 | Equal(t, tt.ArrDive[0].String, "default") 642 | Equal(t, tt.ArrDive[1].String, "default") 643 | 644 | Equal(t, tt.Inner.String, "default") 645 | 646 | tt2 := Test{ 647 | Arr: make([]InnerStruct, 1), 648 | } 649 | 650 | err = set.Struct(context.Background(), &tt2) 651 | Equal(t, err, nil) 652 | Equal(t, len(tt2.Arr), 1) 653 | Equal(t, tt2.Arr[0].String, "") 654 | 655 | tt3 := Test{ 656 | Arr: []InnerStruct{{"ok"}}, 657 | } 658 | 659 | err = set.Struct(context.Background(), &tt3) 660 | Equal(t, err, nil) 661 | Equal(t, len(tt3.Arr), 1) 662 | Equal(t, tt3.Arr[0].String, "ok") 663 | 664 | tt4 := Test{ 665 | ArrDive: []InnerStruct{{"ok"}}, 666 | } 667 | 668 | err = set.Struct(context.Background(), &tt4) 669 | NotEqual(t, err, nil) 670 | Equal(t, err.Error(), "ALREADY OK") 671 | 672 | tt5 := Test{ 673 | ArrNoTag: make([]InnerStruct, 1), 674 | } 675 | 676 | err = set.Struct(context.Background(), &tt5) 677 | Equal(t, err, nil) 678 | Equal(t, len(tt5.ArrNoTag), 1) 679 | Equal(t, tt5.ArrNoTag[0].String, "") 680 | } 681 | -------------------------------------------------------------------------------- /restricted.go: -------------------------------------------------------------------------------- 1 | package mold 2 | 3 | const ( 4 | diveTag = "dive" 5 | restrictedTagChars = ".[],|=+()`~!@#$%^&*\\\"/?<>{}" 6 | tagSeparator = "," 7 | ignoreTag = "-" 8 | tagKeySeparator = "=" 9 | utf8HexComma = "0x2C" 10 | keysTag = "keys" 11 | endKeysTag = "endkeys" 12 | ) 13 | 14 | var ( 15 | restrictedTags = map[string]struct{}{ 16 | diveTag: {}, 17 | ignoreTag: {}, 18 | } 19 | ) 20 | -------------------------------------------------------------------------------- /scrubbers/scrubbers.go: -------------------------------------------------------------------------------- 1 | package scrubbers 2 | 3 | import ( 4 | "github.com/go-playground/mold/v4" 5 | ) 6 | 7 | // New returns a scrubber with defaults registered 8 | func New() *mold.Transformer { 9 | scrub := mold.New() 10 | scrub.SetTagName("scrub") 11 | scrub.Register("emails", emails) 12 | scrub.Register("text", textFn("text")) 13 | scrub.Register("email", textFn("email")) 14 | scrub.Register("name", textFn("name")) 15 | scrub.Register("fname", textFn("fname")) 16 | scrub.Register("lname", textFn("lname")) 17 | return scrub 18 | } 19 | -------------------------------------------------------------------------------- /scrubbers/string.go: -------------------------------------------------------------------------------- 1 | package scrubbers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "regexp" 8 | "strings" 9 | 10 | "github.com/go-playground/mold/v4" 11 | ) 12 | 13 | var ( 14 | emailRegex = regexp.MustCompile("(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:\\(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22)))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+") 15 | emailSubmatchFn = func(input string) string { 16 | idx := strings.IndexByte(input, '@') 17 | scrubbed := fmt.Sprintf("<