Skip to content

Commit b32520d

Browse files
authored
Merge pull request #1 from bnfinet/master
2 parents 16aa3c5 + fcad071 commit b32520d

File tree

2 files changed

+196
-0
lines changed

2 files changed

+196
-0
lines changed

decode.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package httpheader
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"reflect"
7+
"strconv"
8+
"time"
9+
)
10+
11+
var decoderType = reflect.TypeOf(new(Decoder)).Elem()
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, v interface{}) error
17+
}
18+
19+
// DecodeHeader 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 DecodeHeader(header http.Header, v interface{}) error {
22+
val := reflect.ValueOf(v)
23+
for val.Kind() == reflect.Ptr {
24+
val = val.Elem()
25+
}
26+
27+
if val.Kind() != reflect.Struct {
28+
return fmt.Errorf("val is not a struct %+v", val.Kind())
29+
}
30+
return parseValue(header, val)
31+
}
32+
33+
func parseValue(header http.Header, val reflect.Value) error {
34+
typ := val.Type()
35+
for i := 0; i < typ.NumField(); i++ {
36+
sf := typ.Field(i)
37+
if sf.PkgPath != "" && !sf.Anonymous { // unexported
38+
continue
39+
}
40+
41+
sv := val.Field(i)
42+
tag := sf.Tag.Get(tagName)
43+
if tag == "-" {
44+
continue
45+
}
46+
name, opts := parseTag(tag)
47+
if name == "" {
48+
if sf.Anonymous && sv.Kind() == reflect.Struct {
49+
continue
50+
}
51+
name = sf.Name
52+
}
53+
54+
if opts.Contains("omitempty") && header.Get(name) == "" {
55+
continue
56+
}
57+
58+
if sv.Type().Implements(decoderType) {
59+
if !reflect.Indirect(sv).IsValid() {
60+
sv = reflect.New(sv.Type().Elem())
61+
}
62+
63+
m := sv.Interface().(Decoder)
64+
if err := m.DecodeHeader(header, &val); err != nil {
65+
return err
66+
}
67+
continue
68+
}
69+
70+
// TODO: implement iterating over multiple Headers with the same name such as `Cooke:` or `Server:`
71+
// if sv.Kind() == reflect.Slice || sv.Kind() == reflect.Array {
72+
// for k, v := range header[name] {
73+
74+
// }
75+
// continue
76+
// }
77+
78+
for sv.Kind() == reflect.Ptr {
79+
sv = sv.Elem()
80+
}
81+
82+
if sv.Type() == timeType {
83+
h := header.Get(name)
84+
t, err := time.Parse(time.RFC1123, h)
85+
if err != nil {
86+
return err
87+
}
88+
sv.Set(reflect.ValueOf(t))
89+
continue
90+
}
91+
92+
if sv.Kind() == reflect.Struct {
93+
parseValue(header, sv)
94+
continue
95+
}
96+
97+
if sv.Kind() == reflect.Int {
98+
j, err := strconv.Atoi(header.Get(name))
99+
if err != nil {
100+
return err
101+
}
102+
sv.SetInt(int64(j))
103+
}
104+
105+
if sv.Kind() == reflect.String {
106+
sv.SetString(header.Get(name))
107+
}
108+
}
109+
return nil
110+
}

decode_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package httpheader
2+
3+
import (
4+
"net/http"
5+
"reflect"
6+
"testing"
7+
"time"
8+
)
9+
10+
// Event defines a Google Calendar hook event type
11+
type Event string
12+
13+
var pl *GoogleCalendarPayload
14+
15+
// GoogleCalendar hook types
16+
const (
17+
SyncEvent Event = "sync"
18+
ExistsEvent Event = "exists"
19+
NotExistsEvent Event = "not_exists"
20+
)
21+
22+
// GoogleCalendarPayload a google calendar notice
23+
// https://developers.google.com/calendar/v3/push
24+
type GoogleCalendarPayload struct {
25+
ChannelID string `header:"X-Goog-Channel-ID"`
26+
ChannelToken string `header:"X-Goog-Channel-Token,omitempty"`
27+
ChannelExpiration time.Time `header:"X-Goog-Channel-Expiration,omitempty"`
28+
ResourceID string `header:"X-Goog-Resource-ID"`
29+
ResourceURI string `header:"X-Goog-Resource-URI"`
30+
ResourceState string `header:"X-Goog-Resource-State"`
31+
MessageNumber int `header:"X-Goog-Message-Number"`
32+
}
33+
34+
func init() {
35+
pl = &GoogleCalendarPayload{
36+
ChannelID: "channel-ID-value",
37+
ChannelToken: "channel-token-value",
38+
ResourceID: "identifier-for-the-watched-resource",
39+
ResourceURI: "version-specific-URI-of-the-watched-resource",
40+
MessageNumber: 1,
41+
}
42+
pl.ChannelExpiration, _ = time.Parse(time.RFC1123, "Tue, 19 Nov 2013 01:13:52 GMT")
43+
44+
}
45+
46+
func getHeader(e Event) http.Header {
47+
h := http.Header{}
48+
h.Add("X-Goog-Channel-ID", "channel-ID-value")
49+
h.Add("X-Goog-Channel-Token", "channel-token-value")
50+
h.Add("X-Goog-Channel-Expiration", "Tue, 19 Nov 2013 01:13:52 GMT")
51+
h.Add("X-Goog-Resource-ID", "identifier-for-the-watched-resource")
52+
h.Add("X-Goog-Resource-URI", "version-specific-URI-of-the-watched-resource")
53+
h.Add("X-Goog-Message-Number", "1")
54+
h.Add("X-Goog-Resource-State", string(e))
55+
return h
56+
}
57+
58+
func TestDecodeHeader(t *testing.T) {
59+
type args struct {
60+
e Event
61+
}
62+
tests := []struct {
63+
name string
64+
args args
65+
wantErr bool
66+
}{
67+
{"Google Calendar sync", args{SyncEvent}, false},
68+
{"Google Calendar exists", args{ExistsEvent}, false},
69+
{"Google Calendar no exists", args{NotExistsEvent}, false},
70+
}
71+
72+
for i, tt := range tests {
73+
t.Run(tt.name, func(t *testing.T) {
74+
plrun := *pl
75+
plrun.ResourceState = string(tt.args.e)
76+
gcp := GoogleCalendarPayload{}
77+
err := DecodeHeader(getHeader(tt.args.e), &gcp)
78+
if (err != nil) != tt.wantErr {
79+
t.Errorf("%d. DecodeHeader() error = %+v, wantErr %+v", i, err, tt.wantErr)
80+
}
81+
if !reflect.DeepEqual(gcp, plrun) {
82+
t.Errorf("%d. DecodeHeader() does not work as expected, \ngot %+v \nwant %+v", i, gcp, plrun)
83+
}
84+
})
85+
}
86+
}

0 commit comments

Comments
 (0)