pax_global_header00006660000000000000000000000064140031456250014512gustar00rootroot0000000000000052 comment=17b533c501269bc6ff5a59cdef57a343e90fb299 go-httpheader-0.3.0/000077500000000000000000000000001400314562500142455ustar00rootroot00000000000000go-httpheader-0.3.0/.bumpversion.cfg000066400000000000000000000001361400314562500173550ustar00rootroot00000000000000[bumpversion] commit = True tag = True current_version = 0.3.0 [bumpversion:file:encode.go] go-httpheader-0.3.0/.github/000077500000000000000000000000001400314562500156055ustar00rootroot00000000000000go-httpheader-0.3.0/.github/workflows/000077500000000000000000000000001400314562500176425ustar00rootroot00000000000000go-httpheader-0.3.0/.github/workflows/main.yml000066400000000000000000000015321400314562500213120ustar00rootroot00000000000000# This is a basic workflow to help you get started with Actions name: CI on: workflow_dispatch: push: pull_request: jobs: test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: go: ['1.11', '1.12', '1.13', '1.14', '1.15'] steps: - uses: actions/setup-go@v1 with: go-version: ${{ matrix.go }} - uses: actions/checkout@v2 - run: go test -v -coverprofile=profile.cov ./... - name: Send coverage uses: shogo82148/actions-goveralls@v1 with: path-to-profile: profile.cov flag-name: Go-${{ matrix.go }} parallel: true # notifies that all test jobs are finished. finish: needs: test runs-on: ubuntu-latest steps: - uses: shogo82148/actions-goveralls@v1 with: parallel-finished: true go-httpheader-0.3.0/.gitignore000066400000000000000000000004451400314562500162400ustar00rootroot00000000000000# Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test *.prof dist/ cover.html cover.out go-httpheader-0.3.0/CHANGELOG.md000066400000000000000000000010271400314562500160560ustar00rootroot00000000000000# Changelog ## [0.3.0] (2021-01-24) * add `func Decode(header http.Header, v interface{}) error` to support decoding headers into struct ## [0.2.1] (2018-11-03) * add go.mod file to identify as a module ## [0.2.0] (2017-06-24) * support http.Header field. ## 0.1.0 (2017-06-10) * Initial Release [0.2.0]: https://github.com/mozillazg/go-httpheader/compare/v0.1.0...v0.2.0 [0.2.1]: https://github.com/mozillazg/go-httpheader/compare/v0.2.0...v0.2.1 [0.3.0]: https://github.com/mozillazg/go-httpheader/compare/v0.2.1...v0.3.0 go-httpheader-0.3.0/LICENSE000066400000000000000000000020521400314562500152510ustar00rootroot00000000000000MIT License Copyright (c) 2017 mozillazg Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. go-httpheader-0.3.0/Makefile000066400000000000000000000004031400314562500157020ustar00rootroot00000000000000help: @echo "test run test" @echo "lint run lint" .PHONY: test test: go test -race -v -cover -coverprofile cover.out go tool cover -html=cover.out -o cover.html .PHONY: lint lint: gofmt -s -w . goimports -w . golint . go vet go-httpheader-0.3.0/README.md000066400000000000000000000035211400314562500155250ustar00rootroot00000000000000# go-httpheader go-httpheader is a Go library for encoding structs into Header fields. [![Build Status](https://github.com/mozillazg/go-httpheader/workflows/CI/badge.svg?branch=master)](https://github.com/mozillazg/go-httpheader/actions) [![Coverage Status](https://coveralls.io/repos/github/mozillazg/go-httpheader/badge.svg?branch=master)](https://coveralls.io/github/mozillazg/go-httpheader?branch=master) [![Go Report Card](https://goreportcard.com/badge/github.com/mozillazg/go-httpheader)](https://goreportcard.com/report/github.com/mozillazg/go-httpheader) [![GoDoc](https://godoc.org/github.com/mozillazg/go-httpheader?status.svg)](https://godoc.org/github.com/mozillazg/go-httpheader) ## install `go get -u github.com/mozillazg/go-httpheader` ## usage ```go package main import ( "fmt" "net/http" "github.com/mozillazg/go-httpheader" ) type Options struct { hide string ContentType string `header:"Content-Type"` Length int XArray []string `header:"X-Array"` TestHide string `header:"-"` IgnoreEmpty string `header:"X-Empty,omitempty"` IgnoreEmptyN string `header:"X-Empty-N,omitempty"` CustomHeader http.Header } func main() { opt := Options{ hide: "hide", ContentType: "application/json", Length: 2, XArray: []string{"test1", "test2"}, TestHide: "hide", IgnoreEmptyN: "n", CustomHeader: http.Header{ "X-Test-1": []string{"233"}, "X-Test-2": []string{"666"}, }, } h, _ := httpheader.Header(opt) fmt.Printf("%#v", h) // h: // http.Header{ // "X-Test-1": []string{"233"}, // "X-Test-2": []string{"666"}, // "Content-Type": []string{"application/json"}, // "Length": []string{"2"}, // "X-Array": []string{"test1", "test2"}, // "X-Empty-N": []string{"n"}, // } // decode var decode Options httpheader.Decode(h, &decode) } ``` go-httpheader-0.3.0/decode.go000066400000000000000000000125261400314562500160250ustar00rootroot00000000000000package httpheader import ( "fmt" "net/http" "net/textproto" "reflect" "strconv" "time" ) // Decoder is an interface implemented by any type that wishes to decode // itself from Header fields in a non-standard way. type Decoder interface { DecodeHeader(header http.Header, key string) error } // Decode expects to be passed an http.Header and a struct, and parses // header into the struct recursively using the same rules as Header (see above) func Decode(header http.Header, v interface{}) error { val := reflect.ValueOf(v) if val.Kind() != reflect.Ptr || val.IsNil() { return fmt.Errorf("v should be point and should not be nil") } for val.Kind() == reflect.Ptr { val = val.Elem() } if val.Kind() != reflect.Struct { return fmt.Errorf("v is not a struct %+v", val.Kind()) } return parseValue(header, val) } func parseValue(header http.Header, val reflect.Value) error { typ := val.Type() for i := 0; i < typ.NumField(); i++ { sf := typ.Field(i) if sf.PkgPath != "" && !sf.Anonymous { // unexported continue } sv := val.Field(i) tag := sf.Tag.Get(tagName) if tag == "-" { continue } name, opts := parseTag(tag) if name == "" { if sf.Anonymous && sv.Kind() == reflect.Struct { continue } name = sf.Name } if opts.Contains("omitempty") && header.Get(name) == "" { continue } // Decoder interface addr := sv if addr.Kind() != reflect.Ptr && addr.Type().Name() != "" && addr.CanAddr() { addr = addr.Addr() } if addr.Type().NumMethod() > 0 && addr.CanInterface() { if m, ok := addr.Interface().(Decoder); ok { if err := m.DecodeHeader(header, name); err != nil { return err } continue } } if sv.Kind() == reflect.Ptr { ve := reflect.New(sv.Type().Elem()) if err := fillValues(ve, opts, headerValues(header, name)); err != nil { return err } sv.Set(ve) continue } if sv.Type() == timeType { if err := fillValues(sv, opts, headerValues(header, name)); err != nil { return err } continue } if sv.Kind() == reflect.Struct { if err := parseValue(header, sv); err != nil { return err } continue } if err := fillValues(sv, opts, headerValues(header, name)); err != nil { return err } } return nil } func fillValues(sv reflect.Value, opts tagOptions, valArr []string) error { var err error var value string if len(valArr) > 0 { value = valArr[0] } for sv.Kind() == reflect.Ptr { sv = sv.Elem() } switch sv.Kind() { case reflect.Bool: var v bool if opts.Contains("int") { v = value != "0" } else { v = value != "false" } sv.SetBool(v) return nil case reflect.String: sv.SetString(value) return nil case reflect.Uint, reflect.Uint64: var v uint64 if v, err = strconv.ParseUint(value, 10, 64); err != nil { return err } sv.SetUint(v) return nil case reflect.Uint8: var v uint64 if v, err = strconv.ParseUint(value, 10, 8); err != nil { return err } sv.SetUint(v) return nil case reflect.Uint16: var v uint64 if v, err = strconv.ParseUint(value, 10, 16); err != nil { return err } sv.SetUint(v) return nil case reflect.Uint32: var v uint64 if v, err = strconv.ParseUint(value, 10, 32); err != nil { return err } sv.SetUint(v) return nil case reflect.Int, reflect.Int64: var v int64 if v, err = strconv.ParseInt(value, 10, 64); err != nil { return err } sv.SetInt(v) return nil case reflect.Int8: var v int64 if v, err = strconv.ParseInt(value, 10, 8); err != nil { return err } sv.SetInt(v) return nil case reflect.Int16: var v int64 if v, err = strconv.ParseInt(value, 10, 16); err != nil { return err } sv.SetInt(v) return nil case reflect.Int32: var v int64 if v, err = strconv.ParseInt(value, 10, 32); err != nil { return err } sv.SetInt(v) return nil case reflect.Float32: var v float64 if v, err = strconv.ParseFloat(value, 32); err != nil { return err } sv.SetFloat(v) return nil case reflect.Float64: var v float64 if v, err = strconv.ParseFloat(value, 64); err != nil { return err } sv.SetFloat(v) return nil case reflect.Slice: v := reflect.MakeSlice(sv.Type(), len(valArr), len(valArr)) for i, s := range valArr { eleV := reflect.New(sv.Type().Elem()).Elem() if err := fillValues(eleV, opts, []string{s}); err != nil { return err } v.Index(i).Set(eleV) } sv.Set(v) return nil case reflect.Array: v := reflect.Indirect(reflect.New(reflect.ArrayOf(sv.Len(), sv.Type().Elem()))) length := len(valArr) if sv.Len() < length { length = sv.Len() } for i := 0; i < length; i++ { eleV := reflect.New(sv.Type().Elem()).Elem() if err := fillValues(eleV, opts, []string{valArr[i]}); err != nil { return err } v.Index(i).Set(eleV) } sv.Set(v) return nil case reflect.Interface: v := reflect.ValueOf(valArr) sv.Set(v) return nil } if sv.Type() == timeType { var v time.Time if opts.Contains("unix") { u, err := strconv.ParseInt(value, 10, 64) if err != nil { return err } v = time.Unix(u, 0).UTC() } else { v, err = time.Parse(http.TimeFormat, value) if err != nil { return err } } sv.Set(reflect.ValueOf(v)) return nil } // sv.Set(reflect.ValueOf(value)) return nil } func headerValues(h http.Header, key string) []string { return textproto.MIMEHeader(h)[textproto.CanonicalMIMEHeaderKey(key)] } go-httpheader-0.3.0/decode_test.go000066400000000000000000000233661400314562500170700ustar00rootroot00000000000000package httpheader import ( "fmt" "net/http" "net/textproto" "reflect" "regexp" "sort" "testing" "time" ) // Event defines a Google Calendar hook event type type Event string // GoogleCalendar hook types const ( SyncEvent Event = "sync" ExistsEvent Event = "exists" NotExistsEvent Event = "not_exists" ) // GoogleCalendarPayload a google calendar notice // https://developers.google.com/calendar/v3/push type GoogleCalendarPayload struct { ChannelID string `header:"X-Goog-Channel-ID"` ChannelToken string `header:"X-Goog-Channel-Token,omitempty"` ChannelExpiration time.Time `header:"X-Goog-Channel-Expiration,omitempty"` ResourceID string `header:"X-Goog-Resource-ID"` ResourceURI string `header:"X-Goog-Resource-URI"` ResourceState string `header:"X-Goog-Resource-State"` MessageNumber int `header:"X-Goog-Message-Number"` } func getHeader(e Event) http.Header { h := http.Header{} h.Add("X-Goog-Channel-ID", "channel-ID-value") h.Add("X-Goog-Channel-Token", "channel-token-value") h.Add("X-Goog-Channel-Expiration", "Tue, 19 Nov 2013 01:13:52 GMT") h.Add("X-Goog-Resource-ID", "identifier-for-the-watched-resource") h.Add("X-Goog-Resource-URI", "version-specific-URI-of-the-watched-resource") h.Add("X-Goog-Message-Number", "1") h.Add("X-Goog-Resource-State", string(e)) return h } func TestDecodeHeader(t *testing.T) { type args struct { e Event } tests := []struct { name string args args wantErr bool }{ {"Google Calendar sync", args{SyncEvent}, false}, {"Google Calendar exists", args{ExistsEvent}, false}, {"Google Calendar no exists", args{NotExistsEvent}, false}, } for i, tt := range tests { t.Run(tt.name, func(t *testing.T) { plrun := GoogleCalendarPayload{ ChannelID: "channel-ID-value", ChannelToken: "channel-token-value", ResourceID: "identifier-for-the-watched-resource", ResourceURI: "version-specific-URI-of-the-watched-resource", MessageNumber: 1, } plrun.ChannelExpiration, _ = time.Parse(http.TimeFormat, "Tue, 19 Nov 2013 01:13:52 GMT") plrun.ResourceState = string(tt.args.e) gcp := GoogleCalendarPayload{} err := Decode(getHeader(tt.args.e), &gcp) if (err != nil) != tt.wantErr { t.Errorf("%d. Decode() error = %+v, wantErr %+v", i, err, tt.wantErr) } if !reflect.DeepEqual(gcp, plrun) { t.Errorf("%d. Decode() does not work as expected, \ngot %+v \nwant %+v", i, gcp, plrun) } }) } } type DecodedArgs []string func (m *DecodedArgs) DecodeHeader(header http.Header, key string) error { baseKey := textproto.CanonicalMIMEHeaderKey(key) keyMatch := regexp.MustCompile(fmt.Sprintf(`^%s\.\d+$`, baseKey)) var args DecodedArgs for k := range header { if keyMatch.MatchString(textproto.CanonicalMIMEHeaderKey(k)) { args = append(args, header.Get(k)) } } // TODO: sort args by id sort.Strings(args) if len(args) > 0 { *m = args } return nil } func TestDecodeHeader_Unmarshaler(t *testing.T) { type ArgStruct struct { Args DecodedArgs `header:"Arg"` } input := http.Header{ "Arg.0": []string{"a"}, "Arg.1": []string{"b"}, "Arg.2": []string{"c"}, } want := ArgStruct{ Args: []string{"a", "b", "c"}, } var got ArgStruct err := Decode(input, &got) if err != nil { t.Errorf("want no error, got error: %#v", err) } if !reflect.DeepEqual(want, got) { t.Errorf("Decode returned %#v, want %#v", got, want) } } func TestDecodeHeader_UnmarshalerWithNilPointer(t *testing.T) { s := struct { Args *EncodedArgs `header:"Arg"` }{} err := Decode(http.Header{}, s) if err == nil { t.Error("want error but got nil") } } type simpleStruct struct { Foo string } type fullTypeStruct struct { unExport string UnExportTwo string `header:"-"` Bool bool `header:"Bool"` BoolInt bool `header:"Bool-Int,int"` String string StringEmpty string `header:"String-Empty"` StringEmptyIgnore string `header:"String-Empty-Ignore,omitempty"` Uint uint Uint64 uint64 Uint8 uint8 Uint16 uint16 Uint32 uint32 Int int Int64 int64 Int8 int8 Int16 int16 Int32 int32 Float32 float32 Float64 float64 Slice []string SliceTwo []int `header:"Slice-Two"` Array [3]string ArrayTwo [2]int `header:"Array-Two"` Interface interface{} Time time.Time TimeUnix time.Time `header:"Time-Unix,unix"` // Point *string Args DecodedArgs `header:"Arg"` Foo simpleStruct } func TestDecodeHeader_more_data_type(t *testing.T) { timeV := time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC) timeS := "Sat, 01 Jan 2000 12:34:56 GMT" timeU := "946730096" h := http.Header{ "UnExportTwo": []string{"foo"}, "UnExport-Two": []string{"foo"}, "Bool": []string{"true"}, "Bool-Int": []string{"1"}, "String": []string{"foobar"}, "String-Empty": []string{""}, "Uint": []string{"2"}, "Uint64": []string{"3"}, "Uint8": []string{"4"}, "Uint16": []string{"5"}, "Uint32": []string{"6"}, "Int": []string{"7"}, "Int64": []string{"8"}, "Int8": []string{"9"}, "Int16": []string{"10"}, "Int32": []string{"11"}, "Float32": []string{"12.2"}, "Float64": []string{"13.2"}, "Slice": []string{"a", "b", "c"}, "Slice-Two": []string{"1", "2", "3"}, "Array": []string{"a", "b", "c"}, "Array-Two": []string{"1", "2", "3"}, "Interface": []string{"foo", "bar"}, "Time": []string{timeS}, "Time-Unix": []string{timeU}, "Point": []string{"foo"}, "Arg.0": []string{"a"}, "Arg.1": []string{"b"}, "Arg.2": []string{"c"}, "Foo": []string{"bar"}, } want := fullTypeStruct{ unExport: "", UnExportTwo: "", Bool: true, BoolInt: true, String: "foobar", StringEmpty: "", StringEmptyIgnore: "", Uint: 2, Uint64: 3, Uint8: 4, Uint16: 5, Uint32: 6, Int: 7, Int64: 8, Int8: 9, Int16: 10, Int32: 11, Float32: 12.2, Float64: 13.2, Slice: []string{"a", "b", "c"}, SliceTwo: []int{1, 2, 3}, Array: [3]string{"a", "b", "c"}, ArrayTwo: [2]int{1, 2}, Interface: interface{}([]string{"foo", "bar"}), Time: timeV, TimeUnix: timeV, // Point: stringPoint("foo"), Args: []string{"a", "b", "c"}, Foo: simpleStruct{Foo: "bar"}, } var got fullTypeStruct err := Decode(h, &got) if err != nil { t.Errorf("Decode returned error: %#v", err) } if !reflect.DeepEqual(want, got) { t.Errorf("want %#v, but got %#v", want, got) } } func TestDecodeHeader_point(t *testing.T) { type A struct { Point *string } h := http.Header{} h.Set("Point", "foobar") want := A{ Point: stringPoint("foobar"), } var got A err := Decode(h, &got) if err != nil { t.Errorf("Decode returned error: %#v", err) } if !reflect.DeepEqual(want, got) { t.Errorf("want %#v, but got %#v", want, got) } } func stringPoint(s string) *string { return &s } func Test_fillValues_errors(t *testing.T) { type args struct { sv reflect.Value opts tagOptions valArr []string } tests := []struct { name string args args wantErr bool }{ { name: "Uint", args: args{ sv: reflect.New(reflect.TypeOf(uint(3))), opts: tagOptions{}, valArr: []string{"a"}, }, wantErr: true, }, { name: "Uint64", args: args{ sv: reflect.New(reflect.TypeOf(uint64(3))), opts: tagOptions{}, valArr: []string{"a"}, }, wantErr: true, }, { name: "Uint8", args: args{ sv: reflect.New(reflect.TypeOf(uint8(3))), opts: tagOptions{}, valArr: []string{"a"}, }, wantErr: true, }, { name: "Uint16", args: args{ sv: reflect.New(reflect.TypeOf(uint16(3))), opts: tagOptions{}, valArr: []string{"a"}, }, wantErr: true, }, { name: "Uint32", args: args{ sv: reflect.New(reflect.TypeOf(uint32(3))), opts: tagOptions{}, valArr: []string{"a"}, }, wantErr: true, }, { name: "int", args: args{ sv: reflect.New(reflect.TypeOf(int(3))), opts: tagOptions{}, valArr: []string{"a"}, }, wantErr: true, }, { name: "int64", args: args{ sv: reflect.New(reflect.TypeOf(int64(3))), opts: tagOptions{}, valArr: []string{"a"}, }, wantErr: true, }, { name: "int8", args: args{ sv: reflect.New(reflect.TypeOf(int8(3))), opts: tagOptions{}, valArr: []string{"a"}, }, wantErr: true, }, { name: "int16", args: args{ sv: reflect.New(reflect.TypeOf(int16(3))), opts: tagOptions{}, valArr: []string{"a"}, }, wantErr: true, }, { name: "int32", args: args{ sv: reflect.New(reflect.TypeOf(int32(3))), opts: tagOptions{}, valArr: []string{"a"}, }, wantErr: true, }, { name: "float32", args: args{ sv: reflect.New(reflect.TypeOf(float32(3))), opts: tagOptions{}, valArr: []string{"a"}, }, wantErr: true, }, { name: "float64", args: args{ sv: reflect.New(reflect.TypeOf(float64(3))), opts: tagOptions{}, valArr: []string{"a"}, }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if err := fillValues(tt.args.sv, tt.args.opts, tt.args.valArr); (err != nil) != tt.wantErr { t.Errorf("fillValues() error = %v, wantErr %v", err, tt.wantErr) } }) } } go-httpheader-0.3.0/encode.go000066400000000000000000000171621400314562500160400ustar00rootroot00000000000000// Package httpheader implements encoding of structs into http.Header fields. // // As a simple example: // // type Options struct { // ContentType string `header:"Content-Type"` // Length int // } // // opt := Options{"application/json", 2} // h, _ := httpheader.Header(opt) // fmt.Printf("%#v", h) // // will output: // // http.Header{"Content-Type":[]string{"application/json"},"Length":[]string{"2"}} // // The exact mapping between Go values and http.Header is described in the // documentation for the Header() function. package httpheader import ( "fmt" "net/http" "reflect" "strconv" "strings" "time" ) const tagName = "header" // Version ... const Version = "0.3.0" var timeType = reflect.TypeOf(time.Time{}) var headerType = reflect.TypeOf(http.Header{}) var encoderType = reflect.TypeOf(new(Encoder)).Elem() // Encoder is an interface implemented by any type that wishes to encode // itself into Header fields in a non-standard way. type Encoder interface { EncodeHeader(key string, v *http.Header) error } // Header returns the http.Header encoding of v. // // Header expects to be passed a struct, and traverses it recursively using the // following encoding rules. // // Each exported struct field is encoded as a Header field unless // // - the field's tag is "-", or // - the field is empty and its tag specifies the "omitempty" option // // The empty values are false, 0, any nil pointer or interface value, any array // slice, map, or string of length zero, and any time.Time that returns true // for IsZero(). // // The Header field name defaults to the struct field name but can be // specified in the struct field's tag value. The "header" key in the struct // field's tag value is the key name, followed by an optional comma and // options. For example: // // // Field is ignored by this package. // Field int `header:"-"` // // // Field appears as Header field "X-Name". // Field int `header:"X-Name"` // // // Field appears as Header field "X-Name" and the field is omitted if // // its value is empty // Field int `header:"X-Name,omitempty"` // // // Field appears as Header field "Field" (the default), but the field // // is skipped if empty. Note the leading comma. // Field int `header:",omitempty"` // // For encoding individual field values, the following type-dependent rules // apply: // // Boolean values default to encoding as the strings "true" or "false". // Including the "int" option signals that the field should be encoded as the // strings "1" or "0". // // time.Time values default to encoding as RFC1123("Mon, 02 Jan 2006 15:04:05 GMT") // timestamps. Including the "unix" option signals that the field should be // encoded as a Unix time (see time.Unix()) // // Slice and Array values default to encoding as multiple Header values of the // same name. example: // X-Name: []string{"Tom", "Jim"}, etc. // // http.Header values will be used to extend the Header fields. // // Anonymous struct fields are usually encoded as if their inner exported // fields were fields in the outer struct, subject to the standard Go // visibility rules. An anonymous struct field with a name given in its Header // tag is treated as having that name, rather than being anonymous. // // Non-nil pointer values are encoded as the value pointed to. // // All other values are encoded using their default string representation. // // Multiple fields that encode to the same Header filed name will be included // as multiple Header values of the same name. func Header(v interface{}) (http.Header, error) { h := make(http.Header) val := reflect.ValueOf(v) for val.Kind() == reflect.Ptr { if val.IsNil() { return h, nil } val = val.Elem() } if v == nil { return h, nil } if val.Kind() != reflect.Struct { return nil, fmt.Errorf("httpheader: Header() expects struct input. Got %v", val.Kind()) } err := reflectValue(h, val) return h, err } // reflectValue populates the header fields from the struct fields in val. // Embedded structs are followed recursively (using the rules defined in the // Values function documentation) breadth-first. func reflectValue(header http.Header, val reflect.Value) error { var embedded []reflect.Value typ := val.Type() for i := 0; i < typ.NumField(); i++ { sf := typ.Field(i) if sf.PkgPath != "" && !sf.Anonymous { // unexported continue } sv := val.Field(i) tag := sf.Tag.Get(tagName) if tag == "-" { continue } name, opts := parseTag(tag) if name == "" { if sf.Anonymous && sv.Kind() == reflect.Struct { // save embedded struct for later processing embedded = append(embedded, sv) continue } name = sf.Name } if opts.Contains("omitempty") && isEmptyValue(sv) { continue } if sv.Type().Implements(encoderType) { if !reflect.Indirect(sv).IsValid() { sv = reflect.New(sv.Type().Elem()) } m := sv.Interface().(Encoder) if err := m.EncodeHeader(name, &header); err != nil { return err } continue } if sv.Kind() == reflect.Slice || sv.Kind() == reflect.Array { for i := 0; i < sv.Len(); i++ { k := name header.Add(k, valueString(sv.Index(i), opts)) } continue } for sv.Kind() == reflect.Ptr { if sv.IsNil() { break } sv = sv.Elem() } if sv.Type() == timeType { header.Add(name, valueString(sv, opts)) continue } if sv.Type() == headerType { h := sv.Interface().(http.Header) for k, vs := range h { for _, v := range vs { header.Add(k, v) } } continue } if sv.Kind() == reflect.Struct { if err := reflectValue(header, sv); err != nil { return err } continue } header.Add(name, valueString(sv, opts)) } for _, f := range embedded { if err := reflectValue(header, f); err != nil { return err } } return nil } // valueString returns the string representation of a value. func valueString(v reflect.Value, opts tagOptions) string { for v.Kind() == reflect.Ptr { if v.IsNil() { return "" } v = v.Elem() } if v.Kind() == reflect.Bool && opts.Contains("int") { if v.Bool() { return "1" } return "0" } if v.Type() == timeType { t := v.Interface().(time.Time) if opts.Contains("unix") { return strconv.FormatInt(t.Unix(), 10) } return t.Format(http.TimeFormat) } return fmt.Sprint(v.Interface()) } // isEmptyValue checks if a value should be considered empty for the purposes // of omitting fields with the "omitempty" option. func isEmptyValue(v reflect.Value) bool { switch v.Kind() { case reflect.Array, reflect.Map, reflect.Slice, reflect.String: return v.Len() == 0 case reflect.Bool: return !v.Bool() case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return v.Int() == 0 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: return v.Uint() == 0 case reflect.Float32, reflect.Float64: return v.Float() == 0 case reflect.Interface, reflect.Ptr: return v.IsNil() } if v.Type() == timeType { return v.Interface().(time.Time).IsZero() } return false } // tagOptions is the string following a comma in a struct field's "header" tag, or // the empty string. It does not include the leading comma. type tagOptions []string // parseTag splits a struct field's header tag into its name and comma-separated // options. func parseTag(tag string) (string, tagOptions) { s := strings.Split(tag, ",") return s[0], s[1:] } // Contains checks whether the tagOptions contains the specified option. func (o tagOptions) Contains(option string) bool { for _, s := range o { if s == option { return true } } return false } // Encode is an alias of Header function func Encode(v interface{}) (http.Header, error) { return Header(v) } go-httpheader-0.3.0/encode_test.go000066400000000000000000000123651400314562500170770ustar00rootroot00000000000000package httpheader import ( "fmt" "net/http" "reflect" "testing" "time" ) func TestHeader_types(t *testing.T) { str := "string" strPtr := &str timeVal := time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC) tests := []struct { in interface{} want http.Header }{ { // basic primitives struct { A string B int C uint D float32 E bool }{}, http.Header{ "A": []string{""}, "B": []string{"0"}, "C": []string{"0"}, "D": []string{"0"}, "E": []string{"false"}, }, }, { // pointers struct { A *string B *int C **string D *time.Time }{ A: strPtr, C: &strPtr, D: &timeVal, }, http.Header{ "A": []string{str}, "B": []string{""}, "C": []string{str}, "D": []string{"Sat, 01 Jan 2000 12:34:56 GMT"}, }, }, { // slices and arrays struct { A []string B []*string C [2]string D []bool `header:",int"` }{ A: []string{"a", "b"}, B: []*string{&str, &str}, C: [2]string{"a", "b"}, D: []bool{true, false}, }, http.Header{ "A": []string{"a", "b"}, "B": {"string", "string"}, "C": []string{"a", "b"}, "D": {"1", "0"}, }, }, { // other types struct { A time.Time B time.Time `header:",unix"` C bool `header:",int"` D bool `header:",int"` E http.Header }{ A: time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC), B: time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC), C: true, D: false, E: http.Header{ "F": []string{"f1"}, "G": []string{"gg"}, }, }, http.Header{ "A": []string{"Sat, 01 Jan 2000 12:34:56 GMT"}, "B": []string{"946730096"}, "C": []string{"1"}, "D": []string{"0"}, "F": []string{"f1"}, "G": []string{"gg"}, }, }, { nil, http.Header{}, }, { &struct { A string }{"test"}, http.Header{ "A": []string{"test"}, }, }, } for i, tt := range tests { v, err := Header(tt.in) if err != nil { t.Errorf("%d. Header(%+v) returned error: %v", i, tt.in, err) } if !reflect.DeepEqual(tt.want, v) { t.Errorf("%d. Header(%+v) returned %#v, want %#v", i, tt.in, v, tt.want) } } } func TestHeader_omitEmpty(t *testing.T) { str := "" s := struct { a string A string B string `header:",omitempty"` C string `header:"-"` D string `header:"omitempty"` // actually named omitempty, not an option E *string `header:",omitempty"` F bool `header:",omitempty"` G int `header:",omitempty"` H uint `header:",omitempty"` I float32 `header:",omitempty"` J time.Time `header:",omitempty"` K struct{} `header:",omitempty"` }{E: &str} v, err := Header(s) if err != nil { t.Errorf("Header(%#v) returned error: %v", s, err) } want := http.Header{ "A": []string{""}, "Omitempty": []string{""}, "E": []string{""}, // E is included because the pointer is not empty, even though the string being pointed to is } if !reflect.DeepEqual(want, v) { t.Errorf("Header(%#v) returned %v, want %v", s, v, want) } } type A struct { B } type B struct { C string } type D struct { B C string } type e struct { B C string } type F struct { e } func TestHeader_embeddedStructs(t *testing.T) { tests := []struct { in interface{} want http.Header }{ { A{B{C: "foo"}}, http.Header{"C": []string{"foo"}}, }, { D{B: B{C: "bar"}, C: "foo"}, http.Header{"C": []string{"foo", "bar"}}, }, { F{e{B: B{C: "bar"}, C: "foo"}}, // With unexported embed http.Header{"C": []string{"foo", "bar"}}, }, } for i, tt := range tests { v, err := Header(tt.in) if err != nil { t.Errorf("%d. Header(%+v) returned error: %v", i, tt.in, err) } if !reflect.DeepEqual(tt.want, v) { t.Errorf("%d. Header(%+v) returned %v, want %v", i, tt.in, v, tt.want) } } } func TestHeader_invalidInput(t *testing.T) { _, err := Header("") if err == nil { t.Errorf("expected Header() to return an error on invalid input") } } type EncodedArgs []string func (m EncodedArgs) EncodeHeader(key string, v *http.Header) error { for i, arg := range m { v.Set(fmt.Sprintf("%s.%d", key, i), arg) } return nil } func TestHeader_Marshaler(t *testing.T) { s := struct { Args EncodedArgs `header:"Arg"` }{[]string{"a", "b", "c"}} v, err := Header(s) if err != nil { t.Errorf("Header(%+v) returned error: %v", s, err) } want := http.Header{ "Arg.0": []string{"a"}, "Arg.1": []string{"b"}, "Arg.2": []string{"c"}, } if !reflect.DeepEqual(want, v) { t.Errorf("Header(%+v) returned %v, want %v", s, v, want) } } func TestHeader_MarshalerWithNilPointer(t *testing.T) { s := struct { Args *EncodedArgs `header:"Arg"` }{} v, err := Header(s) if err != nil { t.Errorf("Header(%+v) returned error: %v", s, err) } want := http.Header{} if !reflect.DeepEqual(want, v) { t.Errorf("Header(%+v) returned %v, want %v", s, v, want) } } func TestTagParsing(t *testing.T) { name, opts := parseTag("field,foobar,foo") if name != "field" { t.Fatalf("name = %+v, want field", name) } for _, tt := range []struct { opt string want bool }{ {"foobar", true}, {"foo", true}, {"bar", false}, {"field", false}, } { if opts.Contains(tt.opt) != tt.want { t.Errorf("Contains(%+v) = %v", tt.opt, !tt.want) } } } go-httpheader-0.3.0/example_test.go000066400000000000000000000101231400314562500172630ustar00rootroot00000000000000package httpheader_test import ( "fmt" "net/http" "sort" "time" "github.com/mozillazg/go-httpheader" ) func ExampleHeader() { type Options struct { ContentType string `header:"Content-Type"` Length int Bool bool BoolInt bool `header:"Bool-Int,int"` XArray []string `header:"X-Array"` TestHide string `header:"-"` IgnoreEmpty string `header:"X-Empty,omitempty"` IgnoreEmptyN string `header:"X-Empty-N,omitempty"` CreatedAt time.Time `header:"Created-At"` UpdatedAt time.Time `header:"Update-At,unix"` CustomHeader http.Header } opt := Options{ ContentType: "application/json", Length: 2, Bool: true, BoolInt: true, XArray: []string{"test1", "test2"}, TestHide: "hide", IgnoreEmptyN: "n", CreatedAt: time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC), UpdatedAt: time.Date(2001, 1, 1, 12, 34, 56, 0, time.UTC), CustomHeader: http.Header{ "X-Test-1": []string{"233"}, "X-Test-2": []string{"666"}, }, } h, err := httpheader.Header(opt) fmt.Println(err) printHeader(h) // Output: // // Bool: []string{"true"} // Bool-Int: []string{"1"} // Content-Type: []string{"application/json"} // Created-At: []string{"Sat, 01 Jan 2000 12:34:56 GMT"} // Length: []string{"2"} // Update-At: []string{"978352496"} // X-Array: []string{"test1", "test2"} // X-Empty-N: []string{"n"} // X-Test-1: []string{"233"} // X-Test-2: []string{"666"} } func printHeader(h http.Header) { var keys []string for k := range h { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { fmt.Printf("%s: %#v\n", k, h[k]) } } func ExampleDecode() { type Options struct { ContentType string `header:"Content-Type"` Length int Bool bool BoolInt bool `header:"Bool-Int,int"` XArray []string `header:"X-Array"` TestHide string `header:"-"` IgnoreEmpty string `header:"X-Empty,omitempty"` IgnoreEmptyN string `header:"X-Empty-N,omitempty"` CreatedAt time.Time `header:"Created-At"` UpdatedAt time.Time `header:"Update-At,unix"` CustomHeader http.Header } h := http.Header{ "Bool": []string{"true"}, "Bool-Int": []string{"1"}, "X-Test-1": []string{"233"}, "X-Test-2": []string{"666"}, "Content-Type": []string{"application/json"}, "Length": []string{"2"}, "X-Array": []string{"test1", "test2"}, "X-Empty-N": []string{"n"}, "Update-At": []string{"978352496"}, "Created-At": []string{"Sat, 01 Jan 2000 12:34:56 GMT"}, } var opt Options err := httpheader.Decode(h, &opt) fmt.Println(err) fmt.Println(opt.ContentType) fmt.Println(opt.Length) fmt.Println(opt.BoolInt) fmt.Println(opt.XArray) fmt.Println(opt.UpdatedAt) // Output: // // application/json // 2 // true // [test1 test2] // 2001-01-01 12:34:56 +0000 UTC } type EncodedArgs []string func (e EncodedArgs) EncodeHeader(key string, v *http.Header) error { for i, arg := range e { v.Set(fmt.Sprintf("%s.%d", key, i), arg) } return nil } type DecodeArg struct { arg string } func (d *DecodeArg) DecodeHeader(header http.Header, key string) error { value := header.Get(key) d.arg = value return nil } func ExampleEncoder() { // type EncodedArgs []string // // func (e EncodedArgs) EncodeHeader(key string, v *http.Header) error { // for i, arg := range e { // v.Set(fmt.Sprintf("%s.%d", key, i), arg) // } // return nil // } s := struct { Args EncodedArgs `header:"Args"` }{Args: EncodedArgs{"a", "b", "c"}} h, err := httpheader.Header(s) fmt.Println(err) printHeader(h) // Output: // // Args.0: []string{"a"} // Args.1: []string{"b"} // Args.2: []string{"c"} } func ExampleDecoder() { // type DecodeArg struct { // arg string // } // // func (d *DecodeArg) DecodeHeader(header http.Header, key string) error { // value := header.Get(key) // d.arg = value // return nil // } var s struct { Arg DecodeArg } h := http.Header{} h.Set("Arg", "foobar") err := httpheader.Decode(h, &s) fmt.Println(err) fmt.Println(s.Arg.arg) // Output: // // foobar } go-httpheader-0.3.0/go.mod000066400000000000000000000000631400314562500153520ustar00rootroot00000000000000module github.com/mozillazg/go-httpheader go 1.11