├── go.mod ├── .bumpversion.cfg ├── Makefile ├── .gitignore ├── CHANGELOG.md ├── .github └── workflows │ └── main.yml ├── LICENSE ├── README.md ├── example_test.go ├── encode_test.go ├── decode.go ├── encode.go └── decode_test.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mozillazg/go-httpheader 2 | 3 | go 1.11 4 | -------------------------------------------------------------------------------- /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | commit = True 3 | tag = True 4 | current_version = 0.3.1 5 | 6 | [bumpversion:file:encode.go] 7 | 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | help: 2 | @echo "test run test" 3 | @echo "lint run lint" 4 | 5 | .PHONY: test 6 | test: 7 | go test -race -v -cover -coverprofile cover.out 8 | go tool cover -html=cover.out -o cover.html 9 | 10 | .PHONY: lint 11 | lint: 12 | gofmt -s -w . 13 | goimports -w . 14 | golint . 15 | go vet 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | dist/ 26 | cover.html 27 | cover.out 28 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.4.0] (2023-06-29) 4 | 5 | * decode into embedded structs 6 | 7 | 8 | ## [0.3.1] (2022-04-09) 9 | 10 | * fix Decode: don't fill value for struct fields that don't exist in header 11 | 12 | 13 | ## [0.3.0] (2021-01-24) 14 | 15 | * add `func Decode(header http.Header, v interface{}) error` to support decoding headers into struct 16 | 17 | ## [0.2.1] (2018-11-03) 18 | 19 | * add go.mod file to identify as a module 20 | 21 | 22 | ## [0.2.0] (2017-06-24) 23 | 24 | * support http.Header field. 25 | 26 | 27 | ## 0.1.0 (2017-06-10) 28 | 29 | * Initial Release 30 | 31 | [0.2.0]: https://github.com/mozillazg/go-httpheader/compare/v0.1.0...v0.2.0 32 | [0.2.1]: https://github.com/mozillazg/go-httpheader/compare/v0.2.0...v0.2.1 33 | [0.3.0]: https://github.com/mozillazg/go-httpheader/compare/v0.2.1...v0.3.0 34 | [0.3.1]: https://github.com/mozillazg/go-httpheader/compare/v0.3.0...v0.3.1 35 | [0.4.0]: https://github.com/mozillazg/go-httpheader/compare/v0.3.1...v0.4.0 36 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | on: 6 | workflow_dispatch: 7 | push: 8 | pull_request: 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | go: ['1.11', '1.12', '1.13', '1.14', '1.15', '1.16', '1.17', '1.18', '1.19', '1.20'] 17 | 18 | steps: 19 | - uses: actions/setup-go@v4 20 | with: 21 | go-version: ${{ matrix.go }} 22 | - uses: actions/checkout@v3 23 | - run: go test -v -coverprofile=profile.cov ./... 24 | 25 | - name: Send coverage 26 | uses: shogo82148/actions-goveralls@v1 27 | with: 28 | path-to-profile: profile.cov 29 | flag-name: Go-${{ matrix.go }} 30 | parallel: true 31 | 32 | # notifies that all test jobs are finished. 33 | finish: 34 | needs: test 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: shogo82148/actions-goveralls@v1 38 | with: 39 | parallel-finished: true 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 mozillazg 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 | # go-httpheader 2 | 3 | go-httpheader is a Go library for encoding structs into Header fields. 4 | 5 | [![Build Status](https://github.com/mozillazg/go-httpheader/workflows/CI/badge.svg?branch=master)](https://github.com/mozillazg/go-httpheader/actions) 6 | [![Coverage Status](https://coveralls.io/repos/github/mozillazg/go-httpheader/badge.svg?branch=master)](https://coveralls.io/github/mozillazg/go-httpheader?branch=master) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/mozillazg/go-httpheader)](https://goreportcard.com/report/github.com/mozillazg/go-httpheader) 8 | [![GoDoc](https://godoc.org/github.com/mozillazg/go-httpheader?status.svg)](https://godoc.org/github.com/mozillazg/go-httpheader) 9 | 10 | ## install 11 | 12 | ``` 13 | go get github.com/mozillazg/go-httpheader 14 | ``` 15 | 16 | 17 | ## usage 18 | 19 | ```go 20 | package main 21 | 22 | import ( 23 | "fmt" 24 | "net/http" 25 | 26 | "github.com/mozillazg/go-httpheader" 27 | ) 28 | 29 | type Options struct { 30 | hide string 31 | ContentType string `header:"Content-Type"` 32 | Length int 33 | XArray []string `header:"X-Array"` 34 | TestHide string `header:"-"` 35 | IgnoreEmpty string `header:"X-Empty,omitempty"` 36 | IgnoreEmptyN string `header:"X-Empty-N,omitempty"` 37 | CustomHeader http.Header 38 | } 39 | 40 | func main() { 41 | opt := Options{ 42 | hide: "hide", 43 | ContentType: "application/json", 44 | Length: 2, 45 | XArray: []string{"test1", "test2"}, 46 | TestHide: "hide", 47 | IgnoreEmptyN: "n", 48 | CustomHeader: http.Header{ 49 | "X-Test-1": []string{"233"}, 50 | "X-Test-2": []string{"666"}, 51 | }, 52 | } 53 | h, _ := httpheader.Header(opt) 54 | fmt.Printf("%#v", h) 55 | // h: 56 | // http.Header{ 57 | // "X-Test-1": []string{"233"}, 58 | // "X-Test-2": []string{"666"}, 59 | // "Content-Type": []string{"application/json"}, 60 | // "Length": []string{"2"}, 61 | // "X-Array": []string{"test1", "test2"}, 62 | // "X-Empty-N": []string{"n"}, 63 | // } 64 | 65 | // decode 66 | var decode Options 67 | httpheader.Decode(h, &decode) 68 | } 69 | ``` 70 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package httpheader_test 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "sort" 7 | "time" 8 | 9 | "github.com/mozillazg/go-httpheader" 10 | ) 11 | 12 | func ExampleHeader() { 13 | type Options struct { 14 | ContentType string `header:"Content-Type"` 15 | Length int 16 | Bool bool 17 | BoolInt bool `header:"Bool-Int,int"` 18 | XArray []string `header:"X-Array"` 19 | TestHide string `header:"-"` 20 | IgnoreEmpty string `header:"X-Empty,omitempty"` 21 | IgnoreEmptyN string `header:"X-Empty-N,omitempty"` 22 | CreatedAt time.Time `header:"Created-At"` 23 | UpdatedAt time.Time `header:"Update-At,unix"` 24 | CustomHeader http.Header 25 | } 26 | 27 | opt := Options{ 28 | ContentType: "application/json", 29 | Length: 2, 30 | Bool: true, 31 | BoolInt: true, 32 | XArray: []string{"test1", "test2"}, 33 | TestHide: "hide", 34 | IgnoreEmptyN: "n", 35 | CreatedAt: time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC), 36 | UpdatedAt: time.Date(2001, 1, 1, 12, 34, 56, 0, time.UTC), 37 | CustomHeader: http.Header{ 38 | "X-Test-1": []string{"233"}, 39 | "X-Test-2": []string{"666"}, 40 | }, 41 | } 42 | h, err := httpheader.Header(opt) 43 | fmt.Println(err) 44 | printHeader(h) 45 | // Output: 46 | // 47 | // Bool: []string{"true"} 48 | // Bool-Int: []string{"1"} 49 | // Content-Type: []string{"application/json"} 50 | // Created-At: []string{"Sat, 01 Jan 2000 12:34:56 GMT"} 51 | // Length: []string{"2"} 52 | // Update-At: []string{"978352496"} 53 | // X-Array: []string{"test1", "test2"} 54 | // X-Empty-N: []string{"n"} 55 | // X-Test-1: []string{"233"} 56 | // X-Test-2: []string{"666"} 57 | } 58 | 59 | func printHeader(h http.Header) { 60 | var keys []string 61 | for k := range h { 62 | keys = append(keys, k) 63 | } 64 | sort.Strings(keys) 65 | for _, k := range keys { 66 | fmt.Printf("%s: %#v\n", k, h[k]) 67 | } 68 | } 69 | 70 | func ExampleDecode() { 71 | type Options struct { 72 | ContentType string `header:"Content-Type"` 73 | Length int 74 | Bool bool 75 | BoolInt bool `header:"Bool-Int,int"` 76 | XArray []string `header:"X-Array"` 77 | TestHide string `header:"-"` 78 | IgnoreEmpty string `header:"X-Empty,omitempty"` 79 | IgnoreEmptyN string `header:"X-Empty-N,omitempty"` 80 | CreatedAt time.Time `header:"Created-At"` 81 | UpdatedAt time.Time `header:"Update-At,unix"` 82 | CustomHeader http.Header 83 | } 84 | h := http.Header{ 85 | "Bool": []string{"true"}, 86 | "Bool-Int": []string{"1"}, 87 | "X-Test-1": []string{"233"}, 88 | "X-Test-2": []string{"666"}, 89 | "Content-Type": []string{"application/json"}, 90 | "Length": []string{"2"}, 91 | "X-Array": []string{"test1", "test2"}, 92 | "X-Empty-N": []string{"n"}, 93 | "Update-At": []string{"978352496"}, 94 | "Created-At": []string{"Sat, 01 Jan 2000 12:34:56 GMT"}, 95 | } 96 | var opt Options 97 | err := httpheader.Decode(h, &opt) 98 | fmt.Println(err) 99 | fmt.Println(opt.ContentType) 100 | fmt.Println(opt.Length) 101 | fmt.Println(opt.BoolInt) 102 | fmt.Println(opt.XArray) 103 | fmt.Println(opt.UpdatedAt) 104 | // Output: 105 | // 106 | // application/json 107 | // 2 108 | // true 109 | // [test1 test2] 110 | // 2001-01-01 12:34:56 +0000 UTC 111 | } 112 | 113 | type EncodedArgs []string 114 | 115 | func (e EncodedArgs) EncodeHeader(key string, v *http.Header) error { 116 | for i, arg := range e { 117 | v.Set(fmt.Sprintf("%s.%d", key, i), arg) 118 | } 119 | return nil 120 | } 121 | 122 | type DecodeArg struct { 123 | arg string 124 | } 125 | 126 | func (d *DecodeArg) DecodeHeader(header http.Header, key string) error { 127 | value := header.Get(key) 128 | d.arg = value 129 | return nil 130 | } 131 | 132 | func ExampleEncoder() { 133 | // type EncodedArgs []string 134 | // 135 | // func (e EncodedArgs) EncodeHeader(key string, v *http.Header) error { 136 | // for i, arg := range e { 137 | // v.Set(fmt.Sprintf("%s.%d", key, i), arg) 138 | // } 139 | // return nil 140 | // } 141 | 142 | s := struct { 143 | Args EncodedArgs `header:"Args"` 144 | }{Args: EncodedArgs{"a", "b", "c"}} 145 | 146 | h, err := httpheader.Header(s) 147 | fmt.Println(err) 148 | printHeader(h) 149 | // Output: 150 | // 151 | // Args.0: []string{"a"} 152 | // Args.1: []string{"b"} 153 | // Args.2: []string{"c"} 154 | } 155 | 156 | func ExampleDecoder() { 157 | // type DecodeArg struct { 158 | // arg string 159 | // } 160 | // 161 | // func (d *DecodeArg) DecodeHeader(header http.Header, key string) error { 162 | // value := header.Get(key) 163 | // d.arg = value 164 | // return nil 165 | // } 166 | 167 | var s struct { 168 | Arg DecodeArg 169 | } 170 | h := http.Header{} 171 | h.Set("Arg", "foobar") 172 | err := httpheader.Decode(h, &s) 173 | fmt.Println(err) 174 | fmt.Println(s.Arg.arg) 175 | // Output: 176 | // 177 | // foobar 178 | } 179 | -------------------------------------------------------------------------------- /encode_test.go: -------------------------------------------------------------------------------- 1 | package httpheader 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "reflect" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestHeader_types(t *testing.T) { 12 | str := "string" 13 | strPtr := &str 14 | timeVal := time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC) 15 | 16 | tests := []struct { 17 | in interface{} 18 | want http.Header 19 | }{ 20 | { 21 | // basic primitives 22 | struct { 23 | A string 24 | B int 25 | C uint 26 | D float32 27 | E bool 28 | }{}, 29 | http.Header{ 30 | "A": []string{""}, 31 | "B": []string{"0"}, 32 | "C": []string{"0"}, 33 | "D": []string{"0"}, 34 | "E": []string{"false"}, 35 | }, 36 | }, 37 | { 38 | // pointers 39 | struct { 40 | A *string 41 | B *int 42 | C **string 43 | D *time.Time 44 | }{ 45 | A: strPtr, 46 | C: &strPtr, 47 | D: &timeVal, 48 | }, 49 | http.Header{ 50 | "A": []string{str}, 51 | "B": []string{""}, 52 | "C": []string{str}, 53 | "D": []string{"Sat, 01 Jan 2000 12:34:56 GMT"}, 54 | }, 55 | }, 56 | { 57 | // slices and arrays 58 | struct { 59 | A []string 60 | B []*string 61 | C [2]string 62 | D []bool `header:",int"` 63 | }{ 64 | A: []string{"a", "b"}, 65 | B: []*string{&str, &str}, 66 | C: [2]string{"a", "b"}, 67 | D: []bool{true, false}, 68 | }, 69 | http.Header{ 70 | "A": []string{"a", "b"}, 71 | "B": {"string", "string"}, 72 | "C": []string{"a", "b"}, 73 | "D": {"1", "0"}, 74 | }, 75 | }, 76 | { 77 | // other types 78 | struct { 79 | A time.Time 80 | B time.Time `header:",unix"` 81 | C bool `header:",int"` 82 | D bool `header:",int"` 83 | E http.Header 84 | }{ 85 | A: time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC), 86 | B: time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC), 87 | C: true, 88 | D: false, 89 | E: http.Header{ 90 | "F": []string{"f1"}, 91 | "G": []string{"gg"}, 92 | }, 93 | }, 94 | http.Header{ 95 | "A": []string{"Sat, 01 Jan 2000 12:34:56 GMT"}, 96 | "B": []string{"946730096"}, 97 | "C": []string{"1"}, 98 | "D": []string{"0"}, 99 | "F": []string{"f1"}, 100 | "G": []string{"gg"}, 101 | }, 102 | }, 103 | { 104 | nil, 105 | http.Header{}, 106 | }, 107 | { 108 | &struct { 109 | A string 110 | }{"test"}, 111 | http.Header{ 112 | "A": []string{"test"}, 113 | }, 114 | }, 115 | } 116 | 117 | for i, tt := range tests { 118 | v, err := Header(tt.in) 119 | if err != nil { 120 | t.Errorf("%d. Header(%+v) returned error: %v", i, tt.in, err) 121 | } 122 | 123 | if !reflect.DeepEqual(tt.want, v) { 124 | t.Errorf("%d. Header(%+v) returned %#v, want %#v", i, tt.in, v, tt.want) 125 | } 126 | } 127 | } 128 | 129 | func TestHeader_omitEmpty(t *testing.T) { 130 | str := "" 131 | s := struct { 132 | a string 133 | A string 134 | B string `header:",omitempty"` 135 | C string `header:"-"` 136 | D string `header:"omitempty"` // actually named omitempty, not an option 137 | E *string `header:",omitempty"` 138 | F bool `header:",omitempty"` 139 | G int `header:",omitempty"` 140 | H uint `header:",omitempty"` 141 | I float32 `header:",omitempty"` 142 | J time.Time `header:",omitempty"` 143 | K struct{} `header:",omitempty"` 144 | }{E: &str} 145 | 146 | v, err := Header(s) 147 | if err != nil { 148 | t.Errorf("Header(%#v) returned error: %v", s, err) 149 | } 150 | 151 | want := http.Header{ 152 | "A": []string{""}, 153 | "Omitempty": []string{""}, 154 | "E": []string{""}, // E is included because the pointer is not empty, even though the string being pointed to is 155 | } 156 | if !reflect.DeepEqual(want, v) { 157 | t.Errorf("Header(%#v) returned %v, want %v", s, v, want) 158 | } 159 | } 160 | 161 | type A struct { 162 | B 163 | } 164 | 165 | type B struct { 166 | C string 167 | } 168 | 169 | type D struct { 170 | B 171 | C string 172 | } 173 | 174 | type e struct { 175 | B 176 | C string 177 | } 178 | 179 | type F struct { 180 | e 181 | } 182 | 183 | func TestHeader_embeddedStructs(t *testing.T) { 184 | tests := []struct { 185 | in interface{} 186 | want http.Header 187 | }{ 188 | { 189 | A{B{C: "foo"}}, 190 | http.Header{"C": []string{"foo"}}, 191 | }, 192 | { 193 | D{B: B{C: "bar"}, C: "foo"}, 194 | http.Header{"C": []string{"foo", "bar"}}, 195 | }, 196 | { 197 | F{e{B: B{C: "bar"}, C: "foo"}}, // With unexported embed 198 | http.Header{"C": []string{"foo", "bar"}}, 199 | }, 200 | } 201 | 202 | for i, tt := range tests { 203 | v, err := Header(tt.in) 204 | if err != nil { 205 | t.Errorf("%d. Header(%+v) returned error: %v", i, tt.in, err) 206 | } 207 | 208 | if !reflect.DeepEqual(tt.want, v) { 209 | t.Errorf("%d. Header(%+v) returned %v, want %v", i, tt.in, v, tt.want) 210 | } 211 | } 212 | } 213 | 214 | func TestHeader_invalidInput(t *testing.T) { 215 | _, err := Header("") 216 | if err == nil { 217 | t.Errorf("expected Header() to return an error on invalid input") 218 | } 219 | } 220 | 221 | type EncodedArgs []string 222 | 223 | func (m EncodedArgs) EncodeHeader(key string, v *http.Header) error { 224 | for i, arg := range m { 225 | v.Set(fmt.Sprintf("%s.%d", key, i), arg) 226 | } 227 | return nil 228 | } 229 | 230 | func TestHeader_Marshaler(t *testing.T) { 231 | s := struct { 232 | Args EncodedArgs `header:"Arg"` 233 | }{[]string{"a", "b", "c"}} 234 | v, err := Header(s) 235 | if err != nil { 236 | t.Errorf("Header(%+v) returned error: %v", s, err) 237 | } 238 | 239 | want := http.Header{ 240 | "Arg.0": []string{"a"}, 241 | "Arg.1": []string{"b"}, 242 | "Arg.2": []string{"c"}, 243 | } 244 | if !reflect.DeepEqual(want, v) { 245 | t.Errorf("Header(%+v) returned %v, want %v", s, v, want) 246 | } 247 | } 248 | 249 | func TestHeader_MarshalerWithNilPointer(t *testing.T) { 250 | s := struct { 251 | Args *EncodedArgs `header:"Arg"` 252 | }{} 253 | v, err := Header(s) 254 | if err != nil { 255 | t.Errorf("Header(%+v) returned error: %v", s, err) 256 | } 257 | 258 | want := http.Header{} 259 | if !reflect.DeepEqual(want, v) { 260 | t.Errorf("Header(%+v) returned %v, want %v", s, v, want) 261 | } 262 | } 263 | 264 | func TestTagParsing(t *testing.T) { 265 | name, opts := parseTag("field,foobar,foo") 266 | if name != "field" { 267 | t.Fatalf("name = %+v, want field", name) 268 | } 269 | for _, tt := range []struct { 270 | opt string 271 | want bool 272 | }{ 273 | {"foobar", true}, 274 | {"foo", true}, 275 | {"bar", false}, 276 | {"field", false}, 277 | } { 278 | if opts.Contains(tt.opt) != tt.want { 279 | t.Errorf("Contains(%+v) = %v", tt.opt, !tt.want) 280 | } 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /decode.go: -------------------------------------------------------------------------------- 1 | package httpheader 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | "net/textproto" 8 | "reflect" 9 | "strconv" 10 | "time" 11 | ) 12 | 13 | // Decoder is an interface implemented by any type that wishes to decode 14 | // itself from Header fields in a non-standard way. 15 | type Decoder interface { 16 | DecodeHeader(header http.Header, key string) error 17 | } 18 | 19 | // Decode expects to be passed an http.Header and a struct, and parses 20 | // header into the struct recursively using the same rules as Header (see above) 21 | func Decode(header http.Header, v interface{}) error { 22 | val := reflect.ValueOf(v) 23 | if val.Kind() != reflect.Ptr || val.IsNil() { 24 | return errors.New("v should be a pointer and should not be nil") 25 | } 26 | 27 | for val.Kind() == reflect.Ptr { 28 | val = val.Elem() 29 | } 30 | 31 | if val.Kind() != reflect.Struct { 32 | return fmt.Errorf("v is not a struct %+v", val.Kind()) 33 | } 34 | return parseValue(header, val) 35 | } 36 | 37 | // parseValue populates the struct fields in val from the header fields. 38 | // Embedded structs are followed recursively (using the rules defined in the 39 | // Values function documentation) breadth-first. 40 | func parseValue(header http.Header, val reflect.Value) error { 41 | var embedded []reflect.Value 42 | 43 | typ := val.Type() 44 | for i := 0; i < typ.NumField(); i++ { 45 | sf := typ.Field(i) 46 | if sf.PkgPath != "" && !sf.Anonymous { // unexported 47 | continue 48 | } 49 | 50 | sv := val.Field(i) 51 | tag := sf.Tag.Get(tagName) 52 | if tag == "-" { 53 | continue 54 | } 55 | name, opts := parseTag(tag) 56 | if name == "" { 57 | if sf.Anonymous && sv.Kind() == reflect.Struct { 58 | // save embedded struct for later processing 59 | embedded = append(embedded, sv) 60 | continue 61 | } 62 | name = sf.Name 63 | } 64 | 65 | if opts.Contains("omitempty") && header.Get(name) == "" { 66 | continue 67 | } 68 | 69 | // Decoder interface 70 | addr := sv 71 | if addr.Kind() != reflect.Ptr && addr.Type().Name() != "" && addr.CanAddr() { 72 | addr = addr.Addr() 73 | } 74 | if addr.Type().NumMethod() > 0 && addr.CanInterface() { 75 | if m, ok := addr.Interface().(Decoder); ok { 76 | if err := m.DecodeHeader(header, name); err != nil { 77 | return err 78 | } 79 | continue 80 | } 81 | } 82 | 83 | if sv.Kind() == reflect.Ptr { 84 | valArr, exist := headerValues(header, name) 85 | if !exist { 86 | continue 87 | } 88 | ve := reflect.New(sv.Type().Elem()) 89 | if err := fillValues(ve, opts, valArr); err != nil { 90 | return err 91 | } 92 | sv.Set(ve) 93 | continue 94 | } 95 | 96 | if sv.Type() == timeType { 97 | valArr, exist := headerValues(header, name) 98 | if !exist { 99 | continue 100 | } 101 | if err := fillValues(sv, opts, valArr); err != nil { 102 | return err 103 | } 104 | continue 105 | } 106 | 107 | if sv.Kind() == reflect.Struct { 108 | if err := parseValue(header, sv); err != nil { 109 | return err 110 | } 111 | continue 112 | } 113 | 114 | if sv.Kind() != reflect.Slice && sv.Kind() != reflect.Array && sv.Kind() != reflect.Interface { 115 | vals, exist := headerValues(header, name) 116 | if !exist { 117 | continue 118 | } 119 | v := vals[0] 120 | vals = vals[1:] 121 | 122 | if err := fillValues(sv, opts, []string{v}); err != nil { 123 | return err 124 | } 125 | 126 | header.Del(name) 127 | for _, v := range vals { 128 | header.Add(name, v) 129 | } 130 | continue 131 | } 132 | 133 | valArr, exist := headerValues(header, name) 134 | if !exist { 135 | continue 136 | } 137 | if err := fillValues(sv, opts, valArr); err != nil { 138 | return err 139 | } 140 | } 141 | 142 | for _, f := range embedded { 143 | if err := parseValue(header, f); err != nil { 144 | return err 145 | } 146 | } 147 | return nil 148 | } 149 | 150 | func fillValues(sv reflect.Value, opts tagOptions, valArr []string) error { 151 | var err error 152 | var value string 153 | if len(valArr) > 0 { 154 | value = valArr[0] 155 | } 156 | for sv.Kind() == reflect.Ptr { 157 | sv = sv.Elem() 158 | } 159 | 160 | switch sv.Kind() { 161 | case reflect.Bool: 162 | var v bool 163 | if opts.Contains("int") { 164 | v = value != "0" 165 | } else { 166 | v = value != "false" 167 | } 168 | sv.SetBool(v) 169 | return nil 170 | case reflect.String: 171 | sv.SetString(value) 172 | return nil 173 | case reflect.Uint, reflect.Uint64: 174 | var v uint64 175 | if v, err = strconv.ParseUint(value, 10, 64); err != nil { 176 | return err 177 | } 178 | sv.SetUint(v) 179 | return nil 180 | case reflect.Uint8: 181 | var v uint64 182 | if v, err = strconv.ParseUint(value, 10, 8); err != nil { 183 | return err 184 | } 185 | sv.SetUint(v) 186 | return nil 187 | case reflect.Uint16: 188 | var v uint64 189 | if v, err = strconv.ParseUint(value, 10, 16); err != nil { 190 | return err 191 | } 192 | sv.SetUint(v) 193 | return nil 194 | case reflect.Uint32: 195 | var v uint64 196 | if v, err = strconv.ParseUint(value, 10, 32); err != nil { 197 | return err 198 | } 199 | sv.SetUint(v) 200 | return nil 201 | case reflect.Int, reflect.Int64: 202 | var v int64 203 | if v, err = strconv.ParseInt(value, 10, 64); err != nil { 204 | return err 205 | } 206 | sv.SetInt(v) 207 | return nil 208 | case reflect.Int8: 209 | var v int64 210 | if v, err = strconv.ParseInt(value, 10, 8); err != nil { 211 | return err 212 | } 213 | sv.SetInt(v) 214 | return nil 215 | case reflect.Int16: 216 | var v int64 217 | if v, err = strconv.ParseInt(value, 10, 16); err != nil { 218 | return err 219 | } 220 | sv.SetInt(v) 221 | return nil 222 | case reflect.Int32: 223 | var v int64 224 | if v, err = strconv.ParseInt(value, 10, 32); err != nil { 225 | return err 226 | } 227 | sv.SetInt(v) 228 | return nil 229 | case reflect.Float32: 230 | var v float64 231 | if v, err = strconv.ParseFloat(value, 32); err != nil { 232 | return err 233 | } 234 | sv.SetFloat(v) 235 | return nil 236 | case reflect.Float64: 237 | var v float64 238 | if v, err = strconv.ParseFloat(value, 64); err != nil { 239 | return err 240 | } 241 | sv.SetFloat(v) 242 | return nil 243 | case reflect.Slice: 244 | v := reflect.MakeSlice(sv.Type(), len(valArr), len(valArr)) 245 | for i, s := range valArr { 246 | eleV := reflect.New(sv.Type().Elem()).Elem() 247 | if err := fillValues(eleV, opts, []string{s}); err != nil { 248 | return err 249 | } 250 | v.Index(i).Set(eleV) 251 | } 252 | sv.Set(v) 253 | return nil 254 | case reflect.Array: 255 | v := reflect.Indirect(reflect.New(reflect.ArrayOf(sv.Len(), sv.Type().Elem()))) 256 | length := len(valArr) 257 | if sv.Len() < length { 258 | length = sv.Len() 259 | } 260 | for i := 0; i < length; i++ { 261 | eleV := reflect.New(sv.Type().Elem()).Elem() 262 | if err := fillValues(eleV, opts, []string{valArr[i]}); err != nil { 263 | return err 264 | } 265 | v.Index(i).Set(eleV) 266 | } 267 | sv.Set(v) 268 | return nil 269 | case reflect.Interface: 270 | v := reflect.ValueOf(valArr) 271 | sv.Set(v) 272 | return nil 273 | } 274 | 275 | if sv.Type() == timeType { 276 | var v time.Time 277 | if opts.Contains("unix") { 278 | u, err := strconv.ParseInt(value, 10, 64) 279 | if err != nil { 280 | return err 281 | } 282 | v = time.Unix(u, 0).UTC() 283 | } else { 284 | v, err = time.Parse(http.TimeFormat, value) 285 | if err != nil { 286 | return err 287 | } 288 | } 289 | sv.Set(reflect.ValueOf(v)) 290 | return nil 291 | } 292 | 293 | // sv.Set(reflect.ValueOf(value)) 294 | return nil 295 | } 296 | 297 | func headerValues(h http.Header, key string) ([]string, bool) { 298 | vs, ok := textproto.MIMEHeader(h)[textproto.CanonicalMIMEHeaderKey(key)] 299 | return vs, ok 300 | } 301 | -------------------------------------------------------------------------------- /encode.go: -------------------------------------------------------------------------------- 1 | // Package httpheader implements encoding of structs into http.Header fields. 2 | // 3 | // As a simple example: 4 | // 5 | // type Options struct { 6 | // ContentType string `header:"Content-Type"` 7 | // Length int 8 | // } 9 | // 10 | // opt := Options{"application/json", 2} 11 | // h, _ := httpheader.Header(opt) 12 | // fmt.Printf("%#v", h) 13 | // // will output: 14 | // // http.Header{"Content-Type":[]string{"application/json"},"Length":[]string{"2"}} 15 | // 16 | // The exact mapping between Go values and http.Header is described in the 17 | // documentation for the Header() function. 18 | package httpheader 19 | 20 | import ( 21 | "fmt" 22 | "net/http" 23 | "reflect" 24 | "strconv" 25 | "strings" 26 | "time" 27 | ) 28 | 29 | const tagName = "header" 30 | 31 | // Version ... 32 | const Version = "0.3.1" 33 | 34 | var timeType = reflect.TypeOf(time.Time{}) 35 | var headerType = reflect.TypeOf(http.Header{}) 36 | 37 | var encoderType = reflect.TypeOf(new(Encoder)).Elem() 38 | 39 | // Encoder is an interface implemented by any type that wishes to encode 40 | // itself into Header fields in a non-standard way. 41 | type Encoder interface { 42 | EncodeHeader(key string, v *http.Header) error 43 | } 44 | 45 | // Header returns the http.Header encoding of v. 46 | // 47 | // Header expects to be passed a struct, and traverses it recursively using the 48 | // following encoding rules. 49 | // 50 | // Each exported struct field is encoded as a Header field unless 51 | // 52 | // - the field's tag is "-", or 53 | // - the field is empty and its tag specifies the "omitempty" option 54 | // 55 | // The empty values are false, 0, any nil pointer or interface value, any array 56 | // slice, map, or string of length zero, and any time.Time that returns true 57 | // for IsZero(). 58 | // 59 | // The Header field name defaults to the struct field name but can be 60 | // specified in the struct field's tag value. The "header" key in the struct 61 | // field's tag value is the key name, followed by an optional comma and 62 | // options. For example: 63 | // 64 | // // Field is ignored by this package. 65 | // Field int `header:"-"` 66 | // 67 | // // Field appears as Header field "X-Name". 68 | // Field int `header:"X-Name"` 69 | // 70 | // // Field appears as Header field "X-Name" and the field is omitted if 71 | // // its value is empty 72 | // Field int `header:"X-Name,omitempty"` 73 | // 74 | // // Field appears as Header field "Field" (the default), but the field 75 | // // is skipped if empty. Note the leading comma. 76 | // Field int `header:",omitempty"` 77 | // 78 | // For encoding individual field values, the following type-dependent rules 79 | // apply: 80 | // 81 | // Boolean values default to encoding as the strings "true" or "false". 82 | // Including the "int" option signals that the field should be encoded as the 83 | // strings "1" or "0". 84 | // 85 | // time.Time values default to encoding as RFC1123("Mon, 02 Jan 2006 15:04:05 GMT") 86 | // timestamps. Including the "unix" option signals that the field should be 87 | // encoded as a Unix time (see time.Unix()) 88 | // 89 | // Slice and Array values default to encoding as multiple Header values of the 90 | // same name. example: 91 | // X-Name: []string{"Tom", "Jim"}, etc. 92 | // 93 | // http.Header values will be used to extend the Header fields. 94 | // 95 | // Anonymous struct fields are usually encoded as if their inner exported 96 | // fields were fields in the outer struct, subject to the standard Go 97 | // visibility rules. An anonymous struct field with a name given in its Header 98 | // tag is treated as having that name, rather than being anonymous. 99 | // 100 | // Non-nil pointer values are encoded as the value pointed to. 101 | // 102 | // All other values are encoded using their default string representation. 103 | // 104 | // Multiple fields that encode to the same Header filed name will be included 105 | // as multiple Header values of the same name. 106 | func Header(v interface{}) (http.Header, error) { 107 | h := make(http.Header) 108 | val := reflect.ValueOf(v) 109 | for val.Kind() == reflect.Ptr { 110 | if val.IsNil() { 111 | return h, nil 112 | } 113 | val = val.Elem() 114 | } 115 | 116 | if v == nil { 117 | return h, nil 118 | } 119 | 120 | if val.Kind() != reflect.Struct { 121 | return nil, fmt.Errorf("httpheader: Header() expects struct input. Got %v", val.Kind()) 122 | } 123 | 124 | err := reflectValue(h, val) 125 | return h, err 126 | } 127 | 128 | // reflectValue populates the header fields from the struct fields in val. 129 | // Embedded structs are followed recursively (using the rules defined in the 130 | // Values function documentation) breadth-first. 131 | func reflectValue(header http.Header, val reflect.Value) error { 132 | var embedded []reflect.Value 133 | 134 | typ := val.Type() 135 | for i := 0; i < typ.NumField(); i++ { 136 | sf := typ.Field(i) 137 | if sf.PkgPath != "" && !sf.Anonymous { // unexported 138 | continue 139 | } 140 | 141 | sv := val.Field(i) 142 | tag := sf.Tag.Get(tagName) 143 | if tag == "-" { 144 | continue 145 | } 146 | name, opts := parseTag(tag) 147 | if name == "" { 148 | if sf.Anonymous && sv.Kind() == reflect.Struct { 149 | // save embedded struct for later processing 150 | embedded = append(embedded, sv) 151 | continue 152 | } 153 | 154 | name = sf.Name 155 | } 156 | 157 | if opts.Contains("omitempty") && isEmptyValue(sv) { 158 | continue 159 | } 160 | 161 | if sv.Type().Implements(encoderType) { 162 | if !reflect.Indirect(sv).IsValid() { 163 | sv = reflect.New(sv.Type().Elem()) 164 | } 165 | 166 | m := sv.Interface().(Encoder) 167 | if err := m.EncodeHeader(name, &header); err != nil { 168 | return err 169 | } 170 | continue 171 | } 172 | 173 | if sv.Kind() == reflect.Slice || sv.Kind() == reflect.Array { 174 | for i := 0; i < sv.Len(); i++ { 175 | k := name 176 | header.Add(k, valueString(sv.Index(i), opts)) 177 | } 178 | continue 179 | } 180 | 181 | for sv.Kind() == reflect.Ptr { 182 | if sv.IsNil() { 183 | break 184 | } 185 | sv = sv.Elem() 186 | } 187 | 188 | if sv.Type() == timeType { 189 | header.Add(name, valueString(sv, opts)) 190 | continue 191 | } 192 | if sv.Type() == headerType { 193 | h := sv.Interface().(http.Header) 194 | for k, vs := range h { 195 | for _, v := range vs { 196 | header.Add(k, v) 197 | } 198 | } 199 | continue 200 | } 201 | 202 | if sv.Kind() == reflect.Struct { 203 | if err := reflectValue(header, sv); err != nil { 204 | return err 205 | } 206 | continue 207 | } 208 | 209 | header.Add(name, valueString(sv, opts)) 210 | } 211 | 212 | for _, f := range embedded { 213 | if err := reflectValue(header, f); err != nil { 214 | return err 215 | } 216 | } 217 | 218 | return nil 219 | } 220 | 221 | // valueString returns the string representation of a value. 222 | func valueString(v reflect.Value, opts tagOptions) string { 223 | for v.Kind() == reflect.Ptr { 224 | if v.IsNil() { 225 | return "" 226 | } 227 | v = v.Elem() 228 | } 229 | 230 | if v.Kind() == reflect.Bool && opts.Contains("int") { 231 | if v.Bool() { 232 | return "1" 233 | } 234 | return "0" 235 | } 236 | 237 | if v.Type() == timeType { 238 | t := v.Interface().(time.Time) 239 | if opts.Contains("unix") { 240 | return strconv.FormatInt(t.Unix(), 10) 241 | } 242 | return t.Format(http.TimeFormat) 243 | } 244 | 245 | return fmt.Sprint(v.Interface()) 246 | } 247 | 248 | // isEmptyValue checks if a value should be considered empty for the purposes 249 | // of omitting fields with the "omitempty" option. 250 | func isEmptyValue(v reflect.Value) bool { 251 | switch v.Kind() { 252 | case reflect.Array, reflect.Map, reflect.Slice, reflect.String: 253 | return v.Len() == 0 254 | case reflect.Bool: 255 | return !v.Bool() 256 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 257 | return v.Int() == 0 258 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 259 | return v.Uint() == 0 260 | case reflect.Float32, reflect.Float64: 261 | return v.Float() == 0 262 | case reflect.Interface, reflect.Ptr: 263 | return v.IsNil() 264 | } 265 | 266 | if v.Type() == timeType { 267 | return v.Interface().(time.Time).IsZero() 268 | } 269 | 270 | return false 271 | } 272 | 273 | // tagOptions is the string following a comma in a struct field's "header" tag, or 274 | // the empty string. It does not include the leading comma. 275 | type tagOptions []string 276 | 277 | // parseTag splits a struct field's header tag into its name and comma-separated 278 | // options. 279 | func parseTag(tag string) (string, tagOptions) { 280 | s := strings.Split(tag, ",") 281 | return s[0], s[1:] 282 | } 283 | 284 | // Contains checks whether the tagOptions contains the specified option. 285 | func (o tagOptions) Contains(option string) bool { 286 | for _, s := range o { 287 | if s == option { 288 | return true 289 | } 290 | } 291 | return false 292 | } 293 | 294 | // Encode is an alias of Header function 295 | func Encode(v interface{}) (http.Header, error) { 296 | return Header(v) 297 | } 298 | -------------------------------------------------------------------------------- /decode_test.go: -------------------------------------------------------------------------------- 1 | package httpheader 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/textproto" 7 | "reflect" 8 | "regexp" 9 | "sort" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | // Event defines a Google Calendar hook event type 15 | type Event string 16 | 17 | // GoogleCalendar hook types 18 | const ( 19 | SyncEvent Event = "sync" 20 | ExistsEvent Event = "exists" 21 | NotExistsEvent Event = "not_exists" 22 | ) 23 | 24 | // GoogleCalendarPayload a google calendar notice 25 | // https://developers.google.com/calendar/v3/push 26 | type GoogleCalendarPayload struct { 27 | ChannelID string `header:"X-Goog-Channel-ID"` 28 | ChannelToken string `header:"X-Goog-Channel-Token,omitempty"` 29 | ChannelExpiration time.Time `header:"X-Goog-Channel-Expiration,omitempty"` 30 | ResourceID string `header:"X-Goog-Resource-ID"` 31 | ResourceURI string `header:"X-Goog-Resource-URI"` 32 | ResourceState string `header:"X-Goog-Resource-State"` 33 | MessageNumber int `header:"X-Goog-Message-Number"` 34 | } 35 | 36 | func getHeader(e Event) http.Header { 37 | h := http.Header{} 38 | h.Add("X-Goog-Channel-ID", "channel-ID-value") 39 | h.Add("X-Goog-Channel-Token", "channel-token-value") 40 | h.Add("X-Goog-Channel-Expiration", "Tue, 19 Nov 2013 01:13:52 GMT") 41 | h.Add("X-Goog-Resource-ID", "identifier-for-the-watched-resource") 42 | h.Add("X-Goog-Resource-URI", "version-specific-URI-of-the-watched-resource") 43 | h.Add("X-Goog-Message-Number", "1") 44 | h.Add("X-Goog-Resource-State", string(e)) 45 | return h 46 | } 47 | 48 | func TestDecodeHeader(t *testing.T) { 49 | type args struct { 50 | e Event 51 | } 52 | tests := []struct { 53 | name string 54 | args args 55 | wantErr bool 56 | }{ 57 | {"Google Calendar sync", args{SyncEvent}, false}, 58 | {"Google Calendar exists", args{ExistsEvent}, false}, 59 | {"Google Calendar no exists", args{NotExistsEvent}, false}, 60 | } 61 | 62 | for i, tt := range tests { 63 | t.Run(tt.name, func(t *testing.T) { 64 | plrun := GoogleCalendarPayload{ 65 | ChannelID: "channel-ID-value", 66 | ChannelToken: "channel-token-value", 67 | ResourceID: "identifier-for-the-watched-resource", 68 | ResourceURI: "version-specific-URI-of-the-watched-resource", 69 | MessageNumber: 1, 70 | } 71 | plrun.ChannelExpiration, _ = time.Parse(http.TimeFormat, "Tue, 19 Nov 2013 01:13:52 GMT") 72 | plrun.ResourceState = string(tt.args.e) 73 | gcp := GoogleCalendarPayload{} 74 | err := Decode(getHeader(tt.args.e), &gcp) 75 | if (err != nil) != tt.wantErr { 76 | t.Errorf("%d. Decode() error = %+v, wantErr %+v", i, err, tt.wantErr) 77 | } 78 | if !reflect.DeepEqual(gcp, plrun) { 79 | t.Errorf("%d. Decode() does not work as expected, \ngot %+v \nwant %+v", i, gcp, plrun) 80 | } 81 | }) 82 | } 83 | } 84 | 85 | type DecodedArgs []string 86 | 87 | func (m *DecodedArgs) DecodeHeader(header http.Header, key string) error { 88 | baseKey := textproto.CanonicalMIMEHeaderKey(key) 89 | keyMatch := regexp.MustCompile(fmt.Sprintf(`^%s\.\d+$`, baseKey)) 90 | var args DecodedArgs 91 | for k := range header { 92 | if keyMatch.MatchString(textproto.CanonicalMIMEHeaderKey(k)) { 93 | args = append(args, header.Get(k)) 94 | } 95 | } 96 | // TODO: sort args by id 97 | sort.Strings(args) 98 | if len(args) > 0 { 99 | *m = args 100 | } 101 | return nil 102 | } 103 | 104 | func TestDecodeHeader_Unmarshaler(t *testing.T) { 105 | type ArgStruct struct { 106 | Args DecodedArgs `header:"Arg"` 107 | } 108 | input := http.Header{ 109 | "Arg.0": []string{"a"}, 110 | "Arg.1": []string{"b"}, 111 | "Arg.2": []string{"c"}, 112 | } 113 | want := ArgStruct{ 114 | Args: []string{"a", "b", "c"}, 115 | } 116 | var got ArgStruct 117 | 118 | err := Decode(input, &got) 119 | if err != nil { 120 | t.Errorf("want no error, got error: %#v", err) 121 | } 122 | if !reflect.DeepEqual(want, got) { 123 | t.Errorf("Decode returned %#v, want %#v", got, want) 124 | } 125 | } 126 | 127 | func TestDecodeHeader_UnmarshalerWithNilPointer(t *testing.T) { 128 | s := struct { 129 | Args *EncodedArgs `header:"Arg"` 130 | }{} 131 | err := Decode(http.Header{}, s) 132 | if err == nil { 133 | t.Error("want error but got nil") 134 | } 135 | } 136 | 137 | type simpleStruct struct { 138 | Foo string 139 | Bar int 140 | } 141 | 142 | type fullTypeStruct struct { 143 | unExport string 144 | UnExportTwo string `header:"-"` 145 | Bool bool `header:"Bool"` 146 | BoolInt bool `header:"Bool-Int,int"` 147 | String string 148 | StringEmpty string `header:"String-Empty"` 149 | StringEmptyIgnore string `header:"String-Empty-Ignore,omitempty"` 150 | Uint uint 151 | Uint64 uint64 152 | Uint8 uint8 153 | Uint16 uint16 154 | Uint32 uint32 155 | Int int 156 | Int64 int64 157 | Int8 int8 158 | Int16 int16 159 | Int32 int32 160 | Float32 float32 161 | Float64 float64 162 | Slice []string 163 | SliceTwo []int `header:"Slice-Two"` 164 | Array [3]string 165 | ArrayTwo [2]int `header:"Array-Two"` 166 | Interface interface{} 167 | Time time.Time 168 | TimeUnix time.Time `header:"Time-Unix,unix"` 169 | // Point *string 170 | Args DecodedArgs `header:"Arg"` 171 | Foo simpleStruct 172 | } 173 | 174 | func TestDecodeHeader_more_data_type(t *testing.T) { 175 | timeV := time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC) 176 | timeS := "Sat, 01 Jan 2000 12:34:56 GMT" 177 | timeU := "946730096" 178 | h := http.Header{ 179 | "UnExportTwo": []string{"foo"}, 180 | "UnExport-Two": []string{"foo"}, 181 | "Bool": []string{"true"}, 182 | "Bool-Int": []string{"1"}, 183 | "String": []string{"foobar"}, 184 | "String-Empty": []string{""}, 185 | "Uint": []string{"2"}, 186 | "Uint64": []string{"3"}, 187 | "Uint8": []string{"4"}, 188 | "Uint16": []string{"5"}, 189 | "Uint32": []string{"6"}, 190 | "Int": []string{"7"}, 191 | "Int64": []string{"8"}, 192 | "Int8": []string{"9"}, 193 | "Int16": []string{"10"}, 194 | "Int32": []string{"11"}, 195 | "Float32": []string{"12.2"}, 196 | "Float64": []string{"13.2"}, 197 | "Slice": []string{"a", "b", "c"}, 198 | "Slice-Two": []string{"1", "2", "3"}, 199 | "Array": []string{"a", "b", "c"}, 200 | "Array-Two": []string{"1", "2", "3"}, 201 | "Interface": []string{"foo", "bar"}, 202 | "Time": []string{timeS}, 203 | "Time-Unix": []string{timeU}, 204 | "Point": []string{"foo"}, 205 | "Arg.0": []string{"a"}, 206 | "Arg.1": []string{"b"}, 207 | "Arg.2": []string{"c"}, 208 | "Foo": []string{"bar"}, 209 | } 210 | want := fullTypeStruct{ 211 | unExport: "", 212 | UnExportTwo: "", 213 | Bool: true, 214 | BoolInt: true, 215 | String: "foobar", 216 | StringEmpty: "", 217 | StringEmptyIgnore: "", 218 | Uint: 2, 219 | Uint64: 3, 220 | Uint8: 4, 221 | Uint16: 5, 222 | Uint32: 6, 223 | Int: 7, 224 | Int64: 8, 225 | Int8: 9, 226 | Int16: 10, 227 | Int32: 11, 228 | Float32: 12.2, 229 | Float64: 13.2, 230 | Slice: []string{"a", "b", "c"}, 231 | SliceTwo: []int{1, 2, 3}, 232 | Array: [3]string{"a", "b", "c"}, 233 | ArrayTwo: [2]int{1, 2}, 234 | Interface: interface{}([]string{"foo", "bar"}), 235 | Time: timeV, 236 | TimeUnix: timeV, 237 | // Point: stringPoint("foo"), 238 | Args: []string{"a", "b", "c"}, 239 | Foo: simpleStruct{Foo: "bar"}, 240 | } 241 | var got fullTypeStruct 242 | err := Decode(h, &got) 243 | if err != nil { 244 | t.Errorf("Decode returned error: %#v", err) 245 | } 246 | if !reflect.DeepEqual(want, got) { 247 | t.Errorf("want/got:\n%#v\n%#v", want, got) 248 | } 249 | } 250 | 251 | func TestDecodeHeader_point(t *testing.T) { 252 | type A struct { 253 | Point *string 254 | } 255 | h := http.Header{} 256 | h.Set("Point", "foobar") 257 | want := A{ 258 | Point: stringPoint("foobar"), 259 | } 260 | var got A 261 | err := Decode(h, &got) 262 | if err != nil { 263 | t.Errorf("Decode returned error: %#v", err) 264 | } 265 | if !reflect.DeepEqual(want, got) { 266 | t.Errorf("want %#v, but got %#v", want, got) 267 | } 268 | } 269 | func stringPoint(s string) *string { 270 | return &s 271 | } 272 | 273 | func Test_fillValues_errors(t *testing.T) { 274 | type args struct { 275 | sv reflect.Value 276 | opts tagOptions 277 | valArr []string 278 | } 279 | tests := []struct { 280 | name string 281 | args args 282 | wantErr bool 283 | }{ 284 | { 285 | name: "Uint", 286 | args: args{ 287 | sv: reflect.New(reflect.TypeOf(uint(3))), 288 | opts: tagOptions{}, 289 | valArr: []string{"a"}, 290 | }, 291 | wantErr: true, 292 | }, 293 | { 294 | name: "Uint64", 295 | args: args{ 296 | sv: reflect.New(reflect.TypeOf(uint64(3))), 297 | opts: tagOptions{}, 298 | valArr: []string{"a"}, 299 | }, 300 | wantErr: true, 301 | }, 302 | { 303 | name: "Uint8", 304 | args: args{ 305 | sv: reflect.New(reflect.TypeOf(uint8(3))), 306 | opts: tagOptions{}, 307 | valArr: []string{"a"}, 308 | }, 309 | wantErr: true, 310 | }, 311 | { 312 | name: "Uint16", 313 | args: args{ 314 | sv: reflect.New(reflect.TypeOf(uint16(3))), 315 | opts: tagOptions{}, 316 | valArr: []string{"a"}, 317 | }, 318 | wantErr: true, 319 | }, 320 | { 321 | name: "Uint32", 322 | args: args{ 323 | sv: reflect.New(reflect.TypeOf(uint32(3))), 324 | opts: tagOptions{}, 325 | valArr: []string{"a"}, 326 | }, 327 | wantErr: true, 328 | }, 329 | { 330 | name: "int", 331 | args: args{ 332 | sv: reflect.New(reflect.TypeOf(int(3))), 333 | opts: tagOptions{}, 334 | valArr: []string{"a"}, 335 | }, 336 | wantErr: true, 337 | }, 338 | { 339 | name: "int64", 340 | args: args{ 341 | sv: reflect.New(reflect.TypeOf(int64(3))), 342 | opts: tagOptions{}, 343 | valArr: []string{"a"}, 344 | }, 345 | wantErr: true, 346 | }, 347 | { 348 | name: "int8", 349 | args: args{ 350 | sv: reflect.New(reflect.TypeOf(int8(3))), 351 | opts: tagOptions{}, 352 | valArr: []string{"a"}, 353 | }, 354 | wantErr: true, 355 | }, 356 | { 357 | name: "int16", 358 | args: args{ 359 | sv: reflect.New(reflect.TypeOf(int16(3))), 360 | opts: tagOptions{}, 361 | valArr: []string{"a"}, 362 | }, 363 | wantErr: true, 364 | }, 365 | { 366 | name: "int32", 367 | args: args{ 368 | sv: reflect.New(reflect.TypeOf(int32(3))), 369 | opts: tagOptions{}, 370 | valArr: []string{"a"}, 371 | }, 372 | wantErr: true, 373 | }, 374 | { 375 | name: "float32", 376 | args: args{ 377 | sv: reflect.New(reflect.TypeOf(float32(3))), 378 | opts: tagOptions{}, 379 | valArr: []string{"a"}, 380 | }, 381 | wantErr: true, 382 | }, 383 | { 384 | name: "float64", 385 | args: args{ 386 | sv: reflect.New(reflect.TypeOf(float64(3))), 387 | opts: tagOptions{}, 388 | valArr: []string{"a"}, 389 | }, 390 | wantErr: true, 391 | }, 392 | { 393 | name: "slice", 394 | args: args{ 395 | sv: reflect.New(reflect.TypeOf([]int{})), 396 | opts: tagOptions{}, 397 | valArr: []string{"a"}, 398 | }, 399 | wantErr: true, 400 | }, 401 | { 402 | name: "array", 403 | args: args{ 404 | sv: reflect.New(reflect.TypeOf([1]int{})), 405 | opts: tagOptions{}, 406 | valArr: []string{"a"}, 407 | }, 408 | wantErr: true, 409 | }, 410 | { 411 | name: "time", 412 | args: args{ 413 | sv: reflect.New(reflect.TypeOf(time.Time{})), 414 | opts: tagOptions{}, 415 | valArr: []string{"a"}, 416 | }, 417 | wantErr: true, 418 | }, 419 | { 420 | name: "time unix", 421 | args: args{ 422 | sv: reflect.New(reflect.TypeOf(time.Time{})), 423 | opts: tagOptions{"unix"}, 424 | valArr: []string{"a"}, 425 | }, 426 | wantErr: true, 427 | }, 428 | } 429 | for _, tt := range tests { 430 | t.Run(tt.name, func(t *testing.T) { 431 | if err := fillValues(tt.args.sv, tt.args.opts, tt.args.valArr); (err != nil) != tt.wantErr { 432 | t.Errorf("fillValues() error = %v, wantErr %v", err, tt.wantErr) 433 | } 434 | }) 435 | } 436 | } 437 | 438 | func TestDecode_check_header_key_not_present_no_point(t *testing.T) { 439 | h := http.Header{} 440 | h.Set("Length", "100") 441 | 442 | var got fullTypeStruct 443 | err := Decode(h, &got) 444 | if err != nil { 445 | t.Errorf("Decode returned error: %#v", err) 446 | } 447 | 448 | var want fullTypeStruct 449 | if !reflect.DeepEqual(want, got) { 450 | t.Errorf("want %#v, but got %#v", want, got) 451 | } 452 | } 453 | 454 | func TestDecode_check_header_key_not_present_point(t *testing.T) { 455 | type testStruct struct { 456 | A *string 457 | B *fullTypeStruct 458 | C *int 459 | D *[]string 460 | E *[2]string 461 | F interface{} 462 | G *time.Time 463 | } 464 | h := http.Header{} 465 | h.Set("Length", "100") 466 | 467 | var got testStruct 468 | err := Decode(h, &got) 469 | if err != nil { 470 | t.Errorf("Decode returned error: %#v", err) 471 | } 472 | 473 | var want testStruct 474 | if !reflect.DeepEqual(want, got) { 475 | t.Errorf("want %#v, but got %#v", want, got) 476 | } 477 | if got.A != nil || got.B != nil || got.C != nil || got.D != nil || got.E != nil || got.F != nil || got.G != nil { 478 | t.Error("all fields should be nil") 479 | } 480 | } 481 | 482 | func TestDecode_error(t *testing.T) { 483 | h := http.Header{ 484 | "Int": []string{"abc"}, 485 | } 486 | var got fullTypeStruct 487 | err := Decode(h, &got) 488 | if err == nil { 489 | t.Errorf("expect error, got : %#v", got) 490 | } 491 | } 492 | 493 | func TestDecodeHeader_embeddedStructs(t *testing.T) { 494 | tests := []struct { 495 | in http.Header 496 | decode func(http.Header) (interface{}, error) 497 | want interface{} 498 | }{ 499 | { 500 | http.Header{"C": []string{"foo"}}, 501 | func(h http.Header) (interface{}, error) { 502 | var a A 503 | err := Decode(h, &a) 504 | return a, err 505 | }, 506 | A{B{C: "foo"}}, 507 | }, 508 | { 509 | http.Header{"C": []string{"foo"}}, 510 | func(h http.Header) (interface{}, error) { 511 | var d D 512 | err := Decode(h, &d) 513 | return d, err 514 | }, 515 | D{B: B{C: ""}, C: "foo"}, 516 | }, 517 | { 518 | http.Header{"C": []string{"foo", "bar"}}, 519 | func(h http.Header) (interface{}, error) { 520 | var d D 521 | err := Decode(h, &d) 522 | return d, err 523 | }, 524 | D{B: B{C: "bar"}, C: "foo"}, 525 | }, 526 | { 527 | http.Header{"C": []string{"foo", "bar"}}, 528 | func(h http.Header) (interface{}, error) { 529 | var f F 530 | err := Decode(h, &f) 531 | return f, err 532 | }, 533 | F{e{B: B{C: "bar"}, C: "foo"}}, // With unexported embed 534 | }, 535 | { 536 | http.Header{"C": []string{"bar"}}, 537 | func(h http.Header) (interface{}, error) { 538 | var f F 539 | err := Decode(h, &f) 540 | return f, err 541 | }, 542 | F{e{C: "bar"}}, // With unexported embed 543 | }, 544 | } 545 | 546 | for i, tt := range tests { 547 | v, err := tt.decode(tt.in) 548 | if err != nil { 549 | t.Errorf("%d. Header(%+v) returned error: %v", i, tt.in, err) 550 | } 551 | 552 | if !reflect.DeepEqual(tt.want, v) { 553 | t.Errorf("%d. Header(%+v) returned/want:\n%#+v\n%#+v", i, tt.in, v, tt.want) 554 | } 555 | } 556 | } 557 | --------------------------------------------------------------------------------