├── go.mod ├── go.sum ├── .github └── workflows │ └── go.yml ├── LICENSE ├── README.md ├── encodehook.go ├── example_test.go ├── structool.go └── decodehook.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/RussellLuo/structool 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/RussellLuo/mapstructure v1.3.4-0.20230915005935-0ccb59b15cf2 7 | github.com/RussellLuo/structs v1.2.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/RussellLuo/mapstructure v1.3.4-0.20230915005935-0ccb59b15cf2 h1:iwHtW2gVIDynzVcv+6NqhnkvhqhufmEOpbgSsxSdN68= 2 | github.com/RussellLuo/mapstructure v1.3.4-0.20230915005935-0ccb59b15cf2/go.mod h1:Wdvd26olfMFybEGBobXgoTlLLXbWmdp25yEfL3KnrMc= 3 | github.com/RussellLuo/structs v1.2.0 h1:rLR+opKsDCfDUwHCYG2mvFi1xUvYcWSt9kwns4e8OkU= 4 | github.com/RussellLuo/structs v1.2.0/go.mod h1:qas+tRT21XKo0VNnwq/SrsnvG3axjhWbeyKFgb7v20M= 5 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push,pull_request] 3 | jobs: 4 | lint: 5 | name: Lint 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Set up Go 1.14 9 | uses: actions/setup-go@v1 10 | with: 11 | go-version: 1.14 12 | id: go 13 | 14 | - name: Check out code 15 | uses: actions/checkout@v1 16 | 17 | - name: Intsall GolangCI-Lint 18 | run: curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b . latest 19 | 20 | - name: Run lint 21 | run: ./golangci-lint run ./... --skip-dirs benchmarks 22 | 23 | test: 24 | name: Unit Testing 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Set up Go 1.14 28 | uses: actions/setup-go@v1 29 | with: 30 | go-version: 1.14 31 | id: go 32 | 33 | - name: Check out code 34 | uses: actions/checkout@v1 35 | 36 | - name: Get dependencies 37 | run: go get -v -t -d ./... 38 | 39 | - name: Run tests 40 | run: go test -v -race ./... -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Luo Peng 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # structool 2 | 3 | A codec for Go structs with support for chainable encoding/decoding hooks. 4 | 5 | 6 | ## Features 7 | 8 | 1. Provide a uniform codec by combining [mapstructure][1] and [structs][2]. 9 | 2. Make encoding/decoding hooks chainable. 10 | 11 | 12 | ## Installation 13 | 14 | ```bash 15 | $ go get -u github.com/RussellLuo/structool 16 | ``` 17 | 18 | 19 | ## Why?! 20 | 21 | 1. Why to use `structs` 22 | 23 | `mapstructure` has limited support for decoding structs into maps ([issues/166][3] and [issues/249][4]). 24 | 25 | 2. Why to make a fork of `fatih/structs` 26 | 27 | [fatih/structs][5] has been archived, but it does not support encoding hooks yet. 28 | 29 | 3. Why chainable hooks may be useful 30 | 31 | Both `mapstructure` and `structs` support hooks in the form of a single function. While this keeps the libraries themselves simple, it forces us to couple various conversions together. 32 | 33 | Chainable hooks (like HTTP middlewares), on the other hand, promote separation of concerns, and thus make individual hooks reusable and composable. 34 | 35 | 36 | ## Documentation 37 | 38 | Check out the [Godoc][6]. 39 | 40 | 41 | ## License 42 | 43 | [MIT](LICENSE) 44 | 45 | 46 | [1]: https://github.com/mitchellh/mapstructure 47 | [2]: https://github.com/RussellLuo/structs 48 | [3]: https://github.com/mitchellh/mapstructure/issues/166 49 | [4]: https://github.com/mitchellh/mapstructure/issues/249 50 | [5]: https://github.com/fatih/structs 51 | [6]: https://pkg.go.dev/github.com/RussellLuo/structool 52 | -------------------------------------------------------------------------------- /encodehook.go: -------------------------------------------------------------------------------- 1 | package structool 2 | 3 | import ( 4 | "net" 5 | "reflect" 6 | "strconv" 7 | "time" 8 | ) 9 | 10 | func EncodeNumberToString(next EncodeHookFunc) EncodeHookFunc { 11 | return func(value reflect.Value) (interface{}, error) { 12 | switch v := value.Interface().(type) { 13 | case int: 14 | return strconv.Itoa(v), nil 15 | case int8: 16 | return strconv.FormatInt(int64(v), 10), nil 17 | case int16: 18 | return strconv.FormatInt(int64(v), 10), nil 19 | case int32: 20 | return strconv.FormatInt(int64(v), 10), nil 21 | case int64: 22 | return strconv.FormatInt(v, 10), nil 23 | case uint: 24 | return strconv.FormatUint(uint64(v), 10), nil 25 | case uint8: 26 | return strconv.FormatUint(uint64(v), 10), nil 27 | case uint16: 28 | return strconv.FormatUint(uint64(v), 10), nil 29 | case uint32: 30 | return strconv.FormatUint(uint64(v), 10), nil 31 | case uint64: 32 | return strconv.FormatUint(v, 10), nil 33 | case float32: 34 | return strconv.FormatFloat(float64(v), 'f', -1, 32), nil 35 | case float64: 36 | return strconv.FormatFloat(v, 'f', -1, 64), nil 37 | } 38 | 39 | return next(value) 40 | } 41 | } 42 | 43 | func EncodeErrorToString(next EncodeHookFunc) EncodeHookFunc { 44 | return func(value reflect.Value) (interface{}, error) { 45 | if value.Type().Implements(errorInterface) { 46 | v := value.Interface() 47 | if v == nil { 48 | return "", nil 49 | } 50 | return v.(error).Error(), nil 51 | } 52 | 53 | return next(value) 54 | } 55 | } 56 | 57 | func EncodeTimeToString(layout string) func(EncodeHookFunc) EncodeHookFunc { 58 | return func(next EncodeHookFunc) EncodeHookFunc { 59 | return func(value reflect.Value) (interface{}, error) { 60 | switch v := value.Interface().(type) { 61 | case time.Time: 62 | return v.Format(layout), nil 63 | case *time.Time: 64 | if v == nil { 65 | return "", nil 66 | } 67 | return v.Format(layout), nil 68 | } 69 | 70 | return next(value) 71 | } 72 | } 73 | } 74 | 75 | func EncodeDurationToString(next EncodeHookFunc) EncodeHookFunc { 76 | return func(value reflect.Value) (interface{}, error) { 77 | switch v := value.Interface().(type) { 78 | case time.Duration: 79 | return v.String(), nil 80 | case *time.Duration: 81 | if v == nil { 82 | return "", nil 83 | } 84 | return v.String(), nil 85 | } 86 | 87 | return next(value) 88 | } 89 | } 90 | 91 | func EncodeIPToString(next EncodeHookFunc) EncodeHookFunc { 92 | nilToEmpty := func(s string) string { 93 | if s == "" { 94 | return "" 95 | } 96 | return s 97 | } 98 | 99 | return func(value reflect.Value) (interface{}, error) { 100 | switch v := value.Interface().(type) { 101 | case net.IP: 102 | return nilToEmpty(v.String()), nil 103 | case *net.IP: 104 | if v == nil { 105 | return "", nil 106 | } 107 | return nilToEmpty(v.String()), nil 108 | } 109 | 110 | return next(value) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package structool_test 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "time" 7 | 8 | "github.com/RussellLuo/structool" 9 | ) 10 | 11 | func Example_decode() { 12 | in := map[string]interface{}{ 13 | "string": "s", 14 | "bool": true, 15 | "int_str": "10", 16 | "int": 1, 17 | "error": "oops", 18 | "time": "2021-09-29T00:00:00Z", 19 | "duration": "2s", 20 | "ip": "192.168.0.1", 21 | } 22 | out := struct { 23 | String string `structool:"string"` 24 | Bool bool `structool:"bool"` 25 | IntStr int `structool:"int_str"` 26 | Int int `structool:"int"` 27 | Error error `structool:"error"` 28 | Time time.Time `structool:"time"` 29 | Duration time.Duration `structool:"duration"` 30 | IP net.IP `structool:"ip"` 31 | }{} 32 | 33 | codec := structool.New().DecodeHook( 34 | structool.DecodeStringToError, 35 | structool.DecodeStringToTime(time.RFC3339), 36 | structool.DecodeStringToDuration, 37 | structool.DecodeStringToIP, 38 | structool.DecodeStringToNumber, 39 | ) 40 | if err := codec.Decode(in, &out); err != nil { 41 | panic(err) 42 | } 43 | 44 | fmt.Printf("%+v\n", out) 45 | 46 | // Output: 47 | // {String:s Bool:true IntStr:10 Int:1 Error:oops Time:2021-09-29 00:00:00 +0000 UTC Duration:2s IP:192.168.0.1} 48 | } 49 | 50 | func Example_encode() { 51 | in := struct { 52 | String string `structool:"string"` 53 | Bool bool `structool:"bool"` 54 | Int int `structool:"int"` 55 | Error error `structool:"error"` 56 | Time time.Time `structool:"time"` 57 | Duration time.Duration `structool:"duration"` 58 | IP net.IP `structool:"ip"` 59 | }{ 60 | String: "s", 61 | Bool: true, 62 | Int: 1, 63 | Error: fmt.Errorf("oops"), 64 | Time: time.Date(2021, 9, 29, 0, 0, 0, 0, time.UTC), 65 | Duration: 2 * time.Second, 66 | IP: net.IPv4(192, 168, 0, 1), 67 | } 68 | 69 | codec := structool.New().EncodeHook( 70 | structool.EncodeErrorToString, 71 | structool.EncodeTimeToString(time.RFC3339), 72 | structool.EncodeDurationToString, 73 | structool.EncodeIPToString, 74 | ) 75 | out, err := codec.Encode(in) 76 | if err != nil { 77 | panic(err) 78 | } 79 | 80 | fmt.Printf("%#v\n", out) 81 | 82 | // Output: 83 | // map[string]interface {}{"bool":true, "duration":"2s", "error":"oops", "int":1, "ip":"192.168.0.1", "string":"s", "time":"2021-09-29T00:00:00Z"} 84 | } 85 | 86 | func Example_decodeField() { 87 | in := "2s" 88 | var out time.Duration 89 | 90 | codec := structool.New().DecodeHook(structool.DecodeStringToDuration) 91 | if err := codec.Decode(in, &out); err != nil { 92 | panic(err) 93 | } 94 | 95 | fmt.Printf("%#v\n", out) 96 | 97 | // Output: 98 | // 2000000000 99 | } 100 | 101 | func Example_encodeField() { 102 | in := 2 * time.Second 103 | 104 | codec := structool.New().EncodeHook(structool.EncodeDurationToString) 105 | out, err := codec.Encode(in) 106 | if err != nil { 107 | panic(err) 108 | } 109 | 110 | fmt.Printf("%#v\n", out) 111 | 112 | // Output: 113 | // "2s" 114 | } 115 | -------------------------------------------------------------------------------- /structool.go: -------------------------------------------------------------------------------- 1 | package structool 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/RussellLuo/mapstructure" 7 | "github.com/RussellLuo/structs" 8 | ) 9 | 10 | type DecodeHookFunc mapstructure.DecodeHookFuncValue 11 | type EncodeHookFunc structs.EncodeHookFunc 12 | 13 | type Codec struct { 14 | tagName string 15 | 16 | decodeHooks []func(DecodeHookFunc) DecodeHookFunc 17 | encodeHooks []func(EncodeHookFunc) EncodeHookFunc 18 | 19 | decodeHookFunc DecodeHookFunc 20 | encodeHookFunc EncodeHookFunc 21 | } 22 | 23 | func New() *Codec { 24 | return &Codec{ 25 | tagName: "structool", 26 | decodeHookFunc: nilDecodeHookFunc, 27 | encodeHookFunc: nilEncodeHookFunc, 28 | } 29 | } 30 | 31 | func (c *Codec) TagName(name string) *Codec { 32 | c.tagName = name 33 | return c 34 | } 35 | 36 | func (c *Codec) DecodeHook(hooks ...func(DecodeHookFunc) DecodeHookFunc) *Codec { 37 | c.decodeHooks = append(c.decodeHooks, hooks...) 38 | 39 | // Build the final hook function by applying the decoding hooks in the 40 | // order they are passed. 41 | if len(c.decodeHooks) > 0 { 42 | f := c.decodeHooks[len(c.decodeHooks)-1](nilDecodeHookFunc) 43 | for i := len(c.decodeHooks) - 2; i >= 0; i-- { 44 | f = c.decodeHooks[i](f) 45 | } 46 | c.decodeHookFunc = f 47 | } 48 | 49 | return c 50 | } 51 | 52 | func (c *Codec) EncodeHook(hooks ...func(EncodeHookFunc) EncodeHookFunc) *Codec { 53 | c.encodeHooks = append(c.encodeHooks, hooks...) 54 | 55 | // Build the final hook function by applying the encoding hooks in the 56 | // order they are passed. 57 | if len(c.encodeHooks) > 0 { 58 | f := c.encodeHooks[len(c.encodeHooks)-1](nilEncodeHookFunc) 59 | for i := len(c.encodeHooks) - 2; i >= 0; i-- { 60 | f = c.encodeHooks[i](f) 61 | } 62 | c.encodeHookFunc = f 63 | } 64 | 65 | return c 66 | } 67 | 68 | func (c *Codec) Decode(in interface{}, out interface{}) (err error) { 69 | config := &mapstructure.DecoderConfig{ 70 | DecodeHook: c.decodeHookFunc, 71 | Squash: true, // Always squash embedded structs. 72 | TagName: c.tagName, 73 | Result: out, 74 | } 75 | 76 | decoder, err := mapstructure.NewDecoder(config) 77 | if err != nil { 78 | return err 79 | } 80 | 81 | return decoder.Decode(in) 82 | } 83 | 84 | func (c *Codec) Encode(in interface{}) (out interface{}, err error) { 85 | isStruct := isStruct(in) 86 | 87 | if !isStruct { 88 | // Wrap `in` in a struct. 89 | in = struct{ In interface{} }{In: in} 90 | } 91 | 92 | s := structs.New(in) 93 | s.TagName = c.tagName 94 | s.EncodeHook = structs.EncodeHookFunc(c.encodeHookFunc) 95 | 96 | m := s.Map() 97 | 98 | if !isStruct { 99 | for _, v := range m { 100 | // m has one and only one pair of k/v. 101 | return v, nil 102 | } 103 | } 104 | 105 | return m, nil 106 | } 107 | 108 | func isStruct(in interface{}) bool { 109 | v := reflect.ValueOf(in) 110 | 111 | for v.Kind() == reflect.Ptr { 112 | v = v.Elem() 113 | } 114 | 115 | return v.Kind() == reflect.Struct 116 | } 117 | 118 | func nilDecodeHookFunc(from, to reflect.Value) (interface{}, error) { 119 | return from.Interface(), nil 120 | } 121 | 122 | func nilEncodeHookFunc(value reflect.Value) (interface{}, error) { 123 | return value.Interface(), nil 124 | } 125 | -------------------------------------------------------------------------------- /decodehook.go: -------------------------------------------------------------------------------- 1 | package structool 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "reflect" 7 | "strconv" 8 | "time" 9 | ) 10 | 11 | var ( 12 | errorInterface = reflect.TypeOf((*error)(nil)).Elem() 13 | ) 14 | 15 | func DecodeStringToNumber(next DecodeHookFunc) DecodeHookFunc { 16 | return func(from, to reflect.Value) (interface{}, error) { 17 | if from.Kind() != reflect.String { 18 | return next(from, to) 19 | } 20 | 21 | value := from.Interface().(string) 22 | 23 | switch to.Interface().(type) { 24 | case int: 25 | v, err := strconv.Atoi(value) 26 | if err != nil { 27 | return nil, err 28 | } 29 | return v, nil 30 | case int8: 31 | v, err := strconv.ParseInt(value, 10, 8) 32 | if err != nil { 33 | return nil, err 34 | } 35 | return int8(v), nil 36 | case int16: 37 | v, err := strconv.ParseInt(value, 10, 16) 38 | if err != nil { 39 | return nil, err 40 | } 41 | return int16(v), nil 42 | case int32: 43 | v, err := strconv.ParseInt(value, 10, 32) 44 | if err != nil { 45 | return nil, err 46 | } 47 | return int32(v), nil 48 | case int64: 49 | v, err := strconv.ParseInt(value, 10, 64) 50 | if err != nil { 51 | return nil, err 52 | } 53 | return v, nil 54 | case uint: 55 | v, err := strconv.ParseUint(value, 10, 0) 56 | if err != nil { 57 | return nil, err 58 | } 59 | return v, nil 60 | case uint8: 61 | v, err := strconv.ParseUint(value, 10, 8) 62 | if err != nil { 63 | return nil, err 64 | } 65 | return uint8(v), nil 66 | case uint16: 67 | v, err := strconv.ParseUint(value, 10, 16) 68 | if err != nil { 69 | return nil, err 70 | } 71 | return uint16(v), nil 72 | case uint32: 73 | v, err := strconv.ParseUint(value, 10, 32) 74 | if err != nil { 75 | return nil, err 76 | } 77 | return uint32(v), nil 78 | case uint64: 79 | v, err := strconv.ParseUint(value, 10, 64) 80 | if err != nil { 81 | return nil, err 82 | } 83 | return v, nil 84 | case float32: 85 | v, err := strconv.ParseFloat(value, 32) 86 | if err != nil { 87 | return nil, err 88 | } 89 | return float32(v), nil 90 | case float64: 91 | v, err := strconv.ParseFloat(value, 64) 92 | if err != nil { 93 | return nil, err 94 | } 95 | return v, nil 96 | } 97 | 98 | return next(from, to) 99 | } 100 | } 101 | 102 | func DecodeStringToError(next DecodeHookFunc) DecodeHookFunc { 103 | return func(from, to reflect.Value) (interface{}, error) { 104 | if from.Kind() != reflect.String { 105 | return next(from, to) 106 | } 107 | 108 | value := from.Interface().(string) 109 | 110 | if to.Type().Implements(errorInterface) { 111 | if value == "" { 112 | return nil, nil 113 | } 114 | return errors.New(value), nil 115 | } 116 | 117 | return next(from, to) 118 | } 119 | } 120 | 121 | func DecodeStringToTime(layout string) func(DecodeHookFunc) DecodeHookFunc { 122 | return func(next DecodeHookFunc) DecodeHookFunc { 123 | return func(from, to reflect.Value) (interface{}, error) { 124 | if from.Kind() != reflect.String { 125 | return next(from, to) 126 | } 127 | 128 | value := from.Interface().(string) 129 | 130 | switch to.Interface().(type) { 131 | case time.Time: 132 | return time.Parse(layout, value) 133 | case *time.Time: 134 | t, err := time.Parse(layout, value) 135 | if err != nil { 136 | return nil, err 137 | } 138 | return &t, nil 139 | } 140 | 141 | return next(from, to) 142 | } 143 | } 144 | } 145 | 146 | func DecodeStringToDuration(next DecodeHookFunc) DecodeHookFunc { 147 | return func(from, to reflect.Value) (interface{}, error) { 148 | if from.Kind() != reflect.String { 149 | return next(from, to) 150 | } 151 | 152 | value := from.Interface().(string) 153 | 154 | switch to.Interface().(type) { 155 | case time.Duration: 156 | return time.ParseDuration(value) 157 | case *time.Duration: 158 | d, err := time.ParseDuration(value) 159 | if err != nil { 160 | return nil, err 161 | } 162 | return &d, nil 163 | } 164 | 165 | return next(from, to) 166 | } 167 | } 168 | 169 | func DecodeStringToIP(next DecodeHookFunc) DecodeHookFunc { 170 | return func(from, to reflect.Value) (interface{}, error) { 171 | if from.Kind() != reflect.String { 172 | return next(from, to) 173 | } 174 | 175 | value := from.Interface().(string) 176 | 177 | switch to.Interface().(type) { 178 | case net.IP: 179 | return net.ParseIP(value), nil 180 | case *net.IP: 181 | ip := net.ParseIP(value) 182 | return &ip, nil 183 | } 184 | 185 | return next(from, to) 186 | } 187 | } 188 | --------------------------------------------------------------------------------