├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── bench_test.go ├── example_test.go ├── httpreq.go └── httpreq_test.go /.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 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: go 4 | 5 | go: 6 | - 1.4.3 7 | - 1.5.4 8 | - 1.6.3 9 | - tip 10 | 11 | before_install: 12 | - go get -t -v ./... 13 | - go get github.com/axw/gocov/gocov 14 | - go get github.com/mattn/goveralls 15 | - go get golang.org/x/tools/cmd/cover 16 | 17 | script: 18 | - go test -v -covermode=count -coverprofile=profile.cov . 19 | - go tool cover -func profile.cov 20 | - goveralls -coverprofile=profile.cov -service=travis-ci 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Guillaume J. Charmes 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # httpreq 2 | 3 | The package provides an easy way to "unmarshal" query string data into a struct. Without reflect. 4 | 5 | [![GitHub release](https://img.shields.io/github/release/creack/httpreq.svg?maxAge=2592000)]() [![GoDoc](https://godoc.org/github.com/creack/httpreq?status.svg)](https://godoc.org/github.com/creack/httpreq) [![Build Status](https://travis-ci.org/creack/httpreq.svg)](https://travis-ci.org/creack/httpreq) [![Coverage Status](https://coveralls.io/repos/github/creack/httpreq/badge.svg?branch=master)](https://coveralls.io/github/creack/httpreq?branch=master) 6 | 7 | # Example 8 | 9 | ## Literal 10 | 11 | ```go 12 | package main 13 | 14 | import ( 15 | "encoding/json" 16 | "log" 17 | "net/http" 18 | "time" 19 | 20 | "github.com/creack/httpreq" 21 | ) 22 | 23 | // Req is the request query struct. 24 | type Req struct { 25 | Fields []string 26 | Limit int 27 | Page int 28 | Timestamp time.Time 29 | } 30 | 31 | func handler(w http.ResponseWriter, req *http.Request) { 32 | if err := req.ParseForm(); err != nil { 33 | http.Error(w, err.Error(), http.StatusBadRequest) 34 | return 35 | } 36 | data := &Req{} 37 | if err := (httpreq.ParsingMap{ 38 | {Field: "limit", Fct: httpreq.ToInt, Dest: &data.Limit}, 39 | {Field: "page", Fct: httpreq.ToInt, Dest: &data.Page}, 40 | {Field: "fields", Fct: httpreq.ToCommaList, Dest: &data.Fields}, 41 | {Field: "timestamp", Fct: httpreq.ToTSTime, Dest: &data.Timestamp}, 42 | }.Parse(req.Form)); err != nil { 43 | http.Error(w, err.Error(), http.StatusBadRequest) 44 | return 45 | } 46 | _ = json.NewEncoder(w).Encode(data) 47 | } 48 | 49 | func main() { 50 | http.HandleFunc("/", handler) 51 | log.Fatal(http.ListenAndServe(":8080", nil)) 52 | // curl 'http://localhost:8080?timestamp=1437743020&limit=10&page=1&fields=a,b,c' 53 | } 54 | ``` 55 | 56 | ## Chained 57 | 58 | ```go 59 | package main 60 | 61 | import ( 62 | "encoding/json" 63 | "log" 64 | "net/http" 65 | "time" 66 | 67 | "github.com/creack/httpreq" 68 | ) 69 | 70 | // Req is the request query struct. 71 | type Req struct { 72 | Fields []string 73 | Limit int 74 | Page int 75 | Timestamp time.Time 76 | } 77 | 78 | func handler(w http.ResponseWriter, req *http.Request) { 79 | if err := req.ParseForm(); err != nil { 80 | http.Error(w, err.Error(), http.StatusBadRequest) 81 | return 82 | } 83 | 84 | data := &Req{} 85 | if err := httpreq.NewParsingMap(). 86 | Add("limit", httpreq.ToInt, &data.Limit). 87 | Add("page", httpreq.ToInt, &data.Page). 88 | Add("fields", httpreq.ToCommaList, &data.Fields). 89 | Add("timestamp", httpreq.ToTSTime, &data.Timestamp). 90 | Parse(req.Form); err != nil { 91 | http.Error(w, err.Error(), http.StatusBadRequest) 92 | return 93 | } 94 | 95 | _ = json.NewEncoder(w).Encode(data) 96 | } 97 | 98 | func main() { 99 | http.HandleFunc("/", handler) 100 | log.Fatal(http.ListenAndServe(":8080", nil)) 101 | // curl 'http://localhost:8080?timestamp=1437743020&limit=10&page=1&fields=a,b,c' 102 | } 103 | ``` 104 | 105 | # Benchmarks 106 | 107 | ## Single CPU 108 | 109 | ``` 110 | BenchmarkRawLiteral 5000000 410 ns/op 96 B/op 2 allocs/op 111 | BenchmarkRawAdd 1000000 1094 ns/op 384 B/op 5 allocs/op 112 | BenchmarkRawJSONUnmarshal 500000 3038 ns/op 416 B/op 11 allocs/op 113 | ``` 114 | 115 | ## 8 CPUs 116 | 117 | ``` 118 | BenchmarkRawPLiteral-8 5000000 299 ns/op 96 B/op 2 allocs/op 119 | BenchmarkRawPAdd-8 2000000 766 ns/op 384 B/op 5 allocs/op 120 | BenchmarkRawPJSONUnmarshal-8 1000000 1861 ns/op 416 B/op 11 allocs/op 121 | ``` 122 | -------------------------------------------------------------------------------- /bench_test.go: -------------------------------------------------------------------------------- 1 | package httpreq 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | ) 7 | 8 | // Straight benchmark literal. 9 | func BenchmarkRawLiteral(b *testing.B) { 10 | mock := mockForm{ 11 | "limit": "10", 12 | "page": "1", 13 | "fields": "a,b,c", 14 | } 15 | type Req struct { 16 | Fields []string 17 | Limit int 18 | Page int 19 | } 20 | b.ResetTimer() 21 | for i := 0; i < b.N; i++ { 22 | data := &Req{} 23 | if err := (ParsingMap{ 24 | {Field: "limit", Fct: ToInt, Dest: &data.Limit}, 25 | {Field: "page", Fct: ToInt, Dest: &data.Page}, 26 | {Field: "fields", Fct: ToCommaList, Dest: &data.Fields}, 27 | }.Parse(mock)); err != nil { 28 | b.Fatal(err) 29 | } 30 | } 31 | } 32 | 33 | // Straight benchmark Add chain. 34 | func BenchmarkRawAdd(b *testing.B) { 35 | mock := mockForm{ 36 | "limit": "10", 37 | "page": "1", 38 | "fields": "a,b,c", 39 | } 40 | type Req struct { 41 | Fields []string 42 | Limit int 43 | Page int 44 | } 45 | b.ResetTimer() 46 | for i := 0; i < b.N; i++ { 47 | data := &Req{} 48 | if err := NewParsingMap(). 49 | Add("limit", ToInt, &data.Limit). 50 | Add("page", ToInt, &data.Page). 51 | Add("fields", ToCommaList, &data.Fields). 52 | Parse(mock); err != nil { 53 | b.Fatal(err) 54 | } 55 | } 56 | } 57 | 58 | // Straight benchmark json.Unmarshal for comparison. 59 | func BenchmarkRawJSONUnmarshal(b *testing.B) { 60 | mock := []byte(`{"Fields":["a","b","c"],"Limit":10,"Page":1}`) 61 | type Req struct { 62 | Fields []string 63 | Limit int 64 | Page int 65 | } 66 | b.ResetTimer() 67 | for i := 0; i < b.N; i++ { 68 | data := &Req{} 69 | if err := json.Unmarshal(mock, data); err != nil { 70 | b.Fatal(err) 71 | } 72 | } 73 | } 74 | 75 | // Parallel benchmark literal. 76 | func BenchmarkRawPLiteral(b *testing.B) { 77 | mock := mockForm{ 78 | "limit": "10", 79 | "page": "1", 80 | "fields": "a,b,c", 81 | } 82 | type Req struct { 83 | Fields []string 84 | Limit int 85 | Page int 86 | } 87 | b.ResetTimer() 88 | b.RunParallel(func(pb *testing.PB) { 89 | for pb.Next() { 90 | data := &Req{} 91 | if err := (ParsingMap{ 92 | {Field: "limit", Fct: ToInt, Dest: &data.Limit}, 93 | {Field: "page", Fct: ToInt, Dest: &data.Page}, 94 | {Field: "fields", Fct: ToCommaList, Dest: &data.Fields}, 95 | }.Parse(mock)); err != nil { 96 | b.Fatal(err) 97 | } 98 | } 99 | }) 100 | } 101 | 102 | // Parallel benchmark Add chain. 103 | func BenchmarkRawPAdd(b *testing.B) { 104 | mock := mockForm{ 105 | "limit": "10", 106 | "page": "1", 107 | "fields": "a,b,c", 108 | } 109 | type Req struct { 110 | Fields []string 111 | Limit int 112 | Page int 113 | } 114 | b.ResetTimer() 115 | b.RunParallel(func(pb *testing.PB) { 116 | for pb.Next() { 117 | data := &Req{} 118 | if err := NewParsingMap(). 119 | Add("limit", ToInt, &data.Limit). 120 | Add("page", ToInt, &data.Page). 121 | Add("fields", ToCommaList, &data.Fields). 122 | Parse(mock); err != nil { 123 | b.Fatal(err) 124 | } 125 | } 126 | }) 127 | } 128 | 129 | // Parallel benchmark json.Unmarshal for comparison. 130 | func BenchmarkRawPJSONUnmarshal(b *testing.B) { 131 | mock := []byte(`{"Fields":["a","b","c"],"Limit":10,"Page":1}`) 132 | type Req struct { 133 | Fields []string 134 | Limit int 135 | Page int 136 | } 137 | b.ResetTimer() 138 | b.RunParallel(func(pb *testing.PB) { 139 | for pb.Next() { 140 | data := &Req{} 141 | if err := json.Unmarshal(mock, data); err != nil { 142 | b.Fatal(err) 143 | } 144 | } 145 | }) 146 | } 147 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package httpreq_test 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "log" 7 | "net/http" 8 | "net/http/httptest" 9 | "os" 10 | "time" 11 | 12 | "github.com/creack/httpreq" 13 | ) 14 | 15 | func Example() { 16 | origTZ := os.Getenv("TZ") 17 | defer func() { _ = os.Setenv("TZ", origTZ) }() 18 | _ = os.Setenv("TZ", "UTC") 19 | 20 | type Req struct { 21 | Fields []string 22 | Limit int 23 | Page int 24 | Timestamp time.Time 25 | } 26 | hdlr := func(w http.ResponseWriter, req *http.Request) { 27 | _ = req.ParseForm() 28 | data := &Req{} 29 | if err := (httpreq.ParsingMap{ 30 | 0: {Field: "limit", Fct: httpreq.ToInt, Dest: &data.Limit}, 31 | 1: {Field: "page", Fct: httpreq.ToInt, Dest: &data.Page}, 32 | 2: {Field: "fields", Fct: httpreq.ToCommaList, Dest: &data.Fields}, 33 | 3: {Field: "timestamp", Fct: httpreq.ToTSTime, Dest: &data.Timestamp}, 34 | }.Parse(req.Form)); err != nil { 35 | http.Error(w, err.Error(), http.StatusBadRequest) 36 | return 37 | } 38 | _ = json.NewEncoder(w).Encode(data) 39 | } 40 | ts := httptest.NewServer(http.HandlerFunc(hdlr)) 41 | defer ts.Close() 42 | 43 | resp, err := http.Get(ts.URL + "?timestamp=1437743020&limit=10&page=1&fields=a,b,c") 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | if resp.StatusCode != http.StatusOK { 48 | log.Fatalf("Error: %d", resp.StatusCode) 49 | } 50 | _, _ = io.Copy(os.Stdout, resp.Body) 51 | // output: 52 | // {"Fields":["a","b","c"],"Limit":10,"Page":1,"Timestamp":"2015-07-24T13:03:40Z"} 53 | } 54 | 55 | func Example_add() { 56 | type Req struct { 57 | Fields []string 58 | Limit int 59 | Page int 60 | } 61 | hdlr := func(w http.ResponseWriter, req *http.Request) { 62 | data := &Req{} 63 | if err := httpreq.NewParsingMap(). 64 | Add("limit", httpreq.ToInt, &data.Limit). 65 | Add("page", httpreq.ToInt, &data.Page). 66 | Add("fields", httpreq.ToCommaList, &data.Fields). 67 | Parse(req.URL.Query()); err != nil { 68 | http.Error(w, err.Error(), http.StatusBadRequest) 69 | return 70 | } 71 | _ = json.NewEncoder(w).Encode(data) 72 | } 73 | ts := httptest.NewServer(http.HandlerFunc(hdlr)) 74 | defer ts.Close() 75 | 76 | resp, err := http.Get(ts.URL + "?limit=10&page=1&fields=a,b,c") 77 | if err != nil { 78 | log.Fatal(err) 79 | } 80 | if resp.StatusCode != http.StatusOK { 81 | log.Fatalf("Error: %d", resp.StatusCode) 82 | } 83 | _, _ = io.Copy(os.Stdout, resp.Body) 84 | // output: 85 | // {"Fields":["a","b","c"],"Limit":10,"Page":1} 86 | } 87 | 88 | func Example_chain() { 89 | type Req struct { 90 | Fields []string 91 | Limit int 92 | DryRun bool 93 | Threshold float64 94 | Name string 95 | } 96 | hdlr := func(w http.ResponseWriter, req *http.Request) { 97 | data := &Req{} 98 | if err := httpreq.NewParsingMapPre(5). 99 | ToInt("limit", &data.Limit). 100 | ToBool("dryrun", &data.DryRun). 101 | ToFloat64("threshold", &data.Threshold). 102 | ToCommaList("fields", &data.Fields). 103 | ToString("name", &data.Name). 104 | Parse(req.URL.Query()); err != nil { 105 | http.Error(w, err.Error(), http.StatusBadRequest) 106 | return 107 | } 108 | _ = json.NewEncoder(w).Encode(data) 109 | } 110 | ts := httptest.NewServer(http.HandlerFunc(hdlr)) 111 | defer ts.Close() 112 | 113 | resp, err := http.Get(ts.URL + "?limit=10&dryrun=true&fields=a,b,c&threshold=42.5&name=creack") 114 | if err != nil { 115 | log.Fatal(err) 116 | } 117 | if resp.StatusCode != http.StatusOK { 118 | log.Fatalf("Error: %d", resp.StatusCode) 119 | } 120 | _, _ = io.Copy(os.Stdout, resp.Body) 121 | // output: 122 | // {"Fields":["a","b","c"],"Limit":10,"DryRun":true,"Threshold":42.5,"Name":"creack"} 123 | } 124 | -------------------------------------------------------------------------------- /httpreq.go: -------------------------------------------------------------------------------- 1 | // Package httpreq is a set of helper to extract data from HTTP Request. 2 | package httpreq 3 | 4 | import ( 5 | "errors" 6 | "strconv" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | // Common errors. 12 | var ( 13 | ErrWrongType = errors.New("wrong type for the given convertion function") 14 | ) 15 | 16 | // ParsingMapElem represent the needed elements to parse a given element. 17 | // - `Field` to be pulled from the `given Getter() interface` 18 | // - `Fct` the transform function betweend `Getter(Field)` and `Dest` 19 | // - `Dest` where to store the result. 20 | type ParsingMapElem struct { 21 | Field string 22 | Fct func(string, interface{}) error 23 | Dest interface{} 24 | } 25 | 26 | // ParsingMap is a list of ParsingMapElem. 27 | type ParsingMap []ParsingMapElem 28 | 29 | // Getter is the basic interface to extract the intput data. 30 | // Commonly used with http.Request.Form. 31 | type Getter interface { 32 | // Get key return value. 33 | Get(string) string 34 | } 35 | 36 | // NewParsingMap create a new parsing map and returns a pointer to 37 | // be able to call Add directly. 38 | func NewParsingMap() *ParsingMap { 39 | return &ParsingMap{} 40 | } 41 | 42 | // NewParsingMapPre create a new preallocated parsing map and returns a pointer to 43 | // be able to call Add directly. 44 | func NewParsingMapPre(n int) *ParsingMap { 45 | p := make(ParsingMap, 0, n) 46 | return &p 47 | } 48 | 49 | // Add inserts a new field definition in the ParsingMap. 50 | func (p *ParsingMap) Add(field string, fct func(string, interface{}) error, dest interface{}) *ParsingMap { 51 | *p = append(*p, ParsingMapElem{Field: field, Fct: fct, Dest: dest}) 52 | return p 53 | } 54 | 55 | // Parse walks through the ParsingMap and executes it. 56 | func (p ParsingMap) Parse(in Getter) error { 57 | for _, elem := range p { 58 | if e := in.Get(elem.Field); e != "" { 59 | if err := elem.Fct(e, elem.Dest); err != nil { 60 | return err 61 | } 62 | } 63 | } 64 | return nil 65 | } 66 | 67 | // ToCommaList takes the given string and splits it on `,` then set the resulting slice to `dest`. 68 | func ToCommaList(src string, dest interface{}) error { 69 | d, ok := dest.(*[]string) 70 | if !ok { 71 | return ErrWrongType 72 | } 73 | *d = strings.Split(src, ",") 74 | return nil 75 | } 76 | 77 | // ToCommaList is a helper for ToCommaList. 78 | func (p *ParsingMap) ToCommaList(field string, dest interface{}) *ParsingMap { 79 | *p = append(*p, ParsingMapElem{Field: field, Fct: ToCommaList, Dest: dest}) 80 | return p 81 | } 82 | 83 | // ToString takes the given string and sets it to `dest`. 84 | func ToString(src string, dest interface{}) error { 85 | d, ok := dest.(*string) 86 | if !ok { 87 | return ErrWrongType 88 | } 89 | *d = src 90 | return nil 91 | } 92 | 93 | // ToString is a helper for ToString. 94 | func (p *ParsingMap) ToString(field string, dest interface{}) *ParsingMap { 95 | *p = append(*p, ParsingMapElem{Field: field, Fct: ToString, Dest: dest}) 96 | return p 97 | } 98 | 99 | // ToBool takes the given string, parses it as bool and sets it to `dest`. 100 | // NOTE: considers empty/invalid value as false 101 | func ToBool(src string, dest interface{}) error { 102 | d, ok := dest.(*bool) 103 | if !ok { 104 | return ErrWrongType 105 | } 106 | if src == "on" { 107 | *d = true 108 | return nil 109 | } 110 | b, _ := strconv.ParseBool(src) 111 | *d = b 112 | return nil 113 | } 114 | 115 | // ToBool is a helper for ToBool. 116 | func (p *ParsingMap) ToBool(field string, dest interface{}) *ParsingMap { 117 | *p = append(*p, ParsingMapElem{Field: field, Fct: ToBool, Dest: dest}) 118 | return p 119 | } 120 | 121 | // ToInt takes the given string, parses it as int and sets it to `dest`. 122 | func ToInt(src string, dest interface{}) error { 123 | d, ok := dest.(*int) 124 | if !ok { 125 | return ErrWrongType 126 | } 127 | i, err := strconv.Atoi(src) 128 | if err != nil { 129 | return err 130 | } 131 | *d = i 132 | return nil 133 | } 134 | 135 | // ToInt is a helper for ToInt. 136 | func (p *ParsingMap) ToInt(field string, dest interface{}) *ParsingMap { 137 | *p = append(*p, ParsingMapElem{Field: field, Fct: ToInt, Dest: dest}) 138 | return p 139 | } 140 | 141 | // ToFloat64 takes the given string, parses it as float64 and sets it to `dest`. 142 | func ToFloat64(src string, dest interface{}) error { 143 | d, ok := dest.(*float64) 144 | if !ok { 145 | return ErrWrongType 146 | } 147 | f, err := strconv.ParseFloat(src, 64) 148 | if err != nil { 149 | return err 150 | } 151 | *d = f 152 | return nil 153 | } 154 | 155 | // ToFloat64 is a helper for ToFloat64. 156 | func (p *ParsingMap) ToFloat64(field string, dest interface{}) *ParsingMap { 157 | *p = append(*p, ParsingMapElem{Field: field, Fct: ToFloat64, Dest: dest}) 158 | return p 159 | } 160 | 161 | // ToTSTime takes the given string, parses it as timestamp and sets it to `dest`. 162 | func ToTSTime(src string, dest interface{}) error { 163 | ts, err := strconv.ParseInt(src, 10, 64) 164 | if err != nil { 165 | return err 166 | } 167 | t := time.Unix(ts, 0) 168 | 169 | d, ok := dest.(**time.Time) 170 | if !ok { 171 | d, ok := dest.(*time.Time) 172 | if !ok { 173 | return ErrWrongType 174 | } 175 | *d = t 176 | return nil 177 | } 178 | *d = &t 179 | return nil 180 | } 181 | 182 | // ToTSTime is a helper for ToTSTime. 183 | func (p *ParsingMap) ToTSTime(field string, dest interface{}) *ParsingMap { 184 | *p = append(*p, ParsingMapElem{Field: field, Fct: ToTSTime, Dest: dest}) 185 | return p 186 | } 187 | 188 | // ToRFC3339Time takes the given string, parses it as timestamp and sets it to `dest`. 189 | func ToRFC3339Time(src string, dest interface{}) error { 190 | t, err := time.Parse(time.RFC3339, src) 191 | if err != nil { 192 | return err 193 | } 194 | 195 | d, ok := dest.(**time.Time) 196 | if !ok { 197 | d, ok := dest.(*time.Time) 198 | if !ok { 199 | return ErrWrongType 200 | } 201 | *d = t 202 | return nil 203 | } 204 | *d = &t 205 | return nil 206 | } 207 | 208 | // ToRFC3339Time is a helper for ToRFC3339Time. 209 | func (p *ParsingMap) ToRFC3339Time(field string, dest interface{}) *ParsingMap { 210 | *p = append(*p, ParsingMapElem{Field: field, Fct: ToRFC3339Time, Dest: dest}) 211 | return p 212 | } 213 | -------------------------------------------------------------------------------- /httpreq_test.go: -------------------------------------------------------------------------------- 1 | package httpreq 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "os" 9 | "strings" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func TestHandler(t *testing.T) { 15 | origTZ := os.Getenv("TZ") 16 | defer func() { _ = os.Setenv("TZ", origTZ) }() 17 | _ = os.Setenv("TZ", "UTC") 18 | 19 | type Req struct { 20 | Fields []string 21 | Limit int 22 | Page int 23 | Timestamp time.Time 24 | F float64 25 | B bool 26 | Time time.Time 27 | } 28 | hdlr := func(w http.ResponseWriter, req *http.Request) { 29 | if err := req.ParseForm(); err != nil { 30 | http.Error(w, err.Error(), http.StatusBadRequest) 31 | return 32 | } 33 | data := &Req{} 34 | if err := (ParsingMap{ 35 | {Field: "limit", Fct: ToInt, Dest: &data.Limit}, 36 | {Field: "page", Fct: ToInt, Dest: &data.Page}, 37 | {Field: "fields", Fct: ToCommaList, Dest: &data.Fields}, 38 | {Field: "timestamp", Fct: ToTSTime, Dest: &data.Timestamp}, 39 | {Field: "f", Fct: ToFloat64, Dest: &data.F}, 40 | {Field: "b", Fct: ToBool, Dest: &data.B}, 41 | {Field: "t", Fct: ToRFC3339Time, Dest: &data.Time}, 42 | }.Parse(req.Form)); err != nil { 43 | http.Error(w, err.Error(), http.StatusBadRequest) 44 | return 45 | } 46 | _ = json.NewEncoder(w).Encode(data) 47 | } 48 | ts := httptest.NewServer(http.HandlerFunc(hdlr)) 49 | defer ts.Close() 50 | 51 | resp, err := http.Get(ts.URL + "?timestamp=1437743020&limit=10&page=1&fields=a,b,c&f=1.5&b=true&t=2006-01-02T15:04:05Z") 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | if expect, got := resp.StatusCode, http.StatusOK; expect != got { 56 | t.Fatalf("Unexpected result.\nExpect:\t%d\nGot:\t%d\n", expect, got) 57 | } 58 | buf, err := ioutil.ReadAll(resp.Body) 59 | _ = resp.Body.Close() 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | if expect, got := `{"Fields":["a","b","c"],"Limit":10,"Page":1,"Timestamp":"2015-07-24T13:03:40Z","F":1.5,"B":true,"Time":"2006-01-02T15:04:05Z"}`+"\n", string(buf); expect != got { 64 | t.Fatalf("Unexpected result.\nExpect:\t%s\nGot:\t%s\n", expect, got) 65 | } 66 | } 67 | 68 | type mockForm map[string]string 69 | 70 | func (m mockForm) Get(k string) string { return m[k] } 71 | 72 | func TestParseFail(t *testing.T) { 73 | src, dest := "abc", 0 74 | form := mockForm{"limit": src} 75 | if err := (ParsingMap{ 76 | {Field: "limit", Fct: ToInt, Dest: &dest}, 77 | }).Parse(form); err == nil { 78 | t.Fatal("Invalid element in parseMap should yield an error") 79 | } 80 | } 81 | 82 | func TestToCommaList(t *testing.T) { 83 | // List 84 | src, dest := "a,b,c", []string{} 85 | if err := ToCommaList(src, &dest); err != nil { 86 | t.Fatal(err) 87 | } 88 | if expect, got := src, strings.Join(dest, ","); expect != got { 89 | t.Fatalf("Unexpected result.\nExpect:\t%s\nGot:\t%s\n", expect, got) 90 | } 91 | 92 | // Single element list 93 | src, dest = "a", []string{} 94 | if err := ToCommaList(src, &dest); err != nil { 95 | t.Fatal(err) 96 | } 97 | if expect, got := src, strings.Join(dest, ","); expect != got { 98 | t.Fatalf("Unexpected result.\nExpect:\t%s\nGot:\t%s\n", expect, got) 99 | } 100 | 101 | // Empty list 102 | src, dest = "", []string{} 103 | if err := ToCommaList(src, &dest); err != nil { 104 | t.Fatal(err) 105 | } 106 | if expect, got := src, strings.Join(dest, ","); expect != got { 107 | t.Fatalf("Unexpected result.\nExpect:\t%s\nGot:\t%s\n", expect, got) 108 | } 109 | 110 | // Error check 111 | if err := ToCommaList(src, dest); err != ErrWrongType { 112 | t.Fatalf("Wrong type didn't yield the proper error: %v\n", err) 113 | } 114 | } 115 | 116 | func TestToString(t *testing.T) { 117 | src, dest := "hello", "" 118 | if err := ToString(src, &dest); err != nil { 119 | t.Fatal(err) 120 | } 121 | if expect, got := src, dest; expect != got { 122 | t.Fatalf("Unexpected result.\nExpect:\t%s\nGot:\t%s\n", expect, got) 123 | } 124 | 125 | // Error check 126 | if err := ToString(src, dest); err != ErrWrongType { 127 | t.Fatalf("Wrong type didn't yield the proper error: %v\n", err) 128 | } 129 | } 130 | 131 | func TestToBool(t *testing.T) { 132 | // "true" 133 | src, dest := "true", false 134 | if err := ToBool(src, &dest); err != nil { 135 | t.Fatal(err) 136 | } 137 | if expect, got := true, dest; expect != got { 138 | t.Fatalf("Unexpected result.\nExpect:\t%t\nGot:\t%t\n", expect, got) 139 | } 140 | 141 | // "1" -> true 142 | src, dest = "1", false 143 | if err := ToBool(src, &dest); err != nil { 144 | t.Fatal(err) 145 | } 146 | if expect, got := true, dest; expect != got { 147 | t.Fatalf("Unexpected result.\nExpect:\t%t\nGot:\t%t\n", expect, got) 148 | } 149 | 150 | // "on" -> true 151 | src, dest = "on", false 152 | if err := ToBool(src, &dest); err != nil { 153 | t.Fatal(err) 154 | } 155 | if expect, got := true, dest; expect != got { 156 | t.Fatalf("Unexpected result.\nExpect:\t%t\nGot:\t%t\n", expect, got) 157 | } 158 | 159 | // "false" -> false 160 | src, dest = "false", true 161 | if err := ToBool(src, &dest); err != nil { 162 | t.Fatal(err) 163 | } 164 | if expect, got := false, dest; expect != got { 165 | t.Fatalf("Unexpected result.\nExpect:\t%t\nGot:\t%t\n", expect, got) 166 | } 167 | 168 | // emtpy -> false 169 | src, dest = "", true 170 | if err := ToBool(src, &dest); err != nil { 171 | t.Fatal(err) 172 | } 173 | if expect, got := false, dest; expect != got { 174 | t.Fatalf("Unexpected result.\nExpect:\t%t\nGot:\t%t\n", expect, got) 175 | } 176 | 177 | // random -> false 178 | src, dest = "abcd", true 179 | if err := ToBool(src, &dest); err != nil { 180 | t.Fatal(err) 181 | } 182 | if expect, got := false, dest; expect != got { 183 | t.Fatalf("Unexpected result.\nExpect:\t%t\nGot:\t%t\n", expect, got) 184 | } 185 | 186 | // Error check 187 | if err := ToBool(src, dest); err != ErrWrongType { 188 | t.Fatalf("Wrong type didn't yield the proper error: %v\n", err) 189 | } 190 | } 191 | 192 | func TestToInt(t *testing.T) { 193 | src, dest := "42", 0 194 | if err := ToInt(src, &dest); err != nil { 195 | t.Fatal(err) 196 | } 197 | if expect, got := 42, dest; expect != got { 198 | t.Fatalf("Unexpected result.\nExpect:\t%d\nGot:\t%d\n", expect, got) 199 | } 200 | 201 | // Error check 202 | src, dest = "abc", -1 203 | if err := ToInt(src, &dest); err == nil { 204 | t.Fatal("Invalid integer should yield an error") 205 | } 206 | 207 | if err := ToInt(src, dest); err != ErrWrongType { 208 | t.Fatalf("Wrong type didn't yield the proper error: %v\n", err) 209 | } 210 | } 211 | 212 | func TestToFloat64(t *testing.T) { 213 | src, dest := "42.", 0. 214 | if err := ToFloat64(src, &dest); err != nil { 215 | t.Fatal(err) 216 | } 217 | if expect, got := 42., dest; expect != got { 218 | t.Fatalf("Unexpected result.\nExpect:\t%f\nGot:\t%f\n", expect, got) 219 | } 220 | 221 | // Error check 222 | src, dest = "abc", -1. 223 | if err := ToFloat64(src, &dest); err == nil { 224 | t.Fatal("Invalid integer should yield an error") 225 | } 226 | 227 | if err := ToFloat64(src, dest); err != ErrWrongType { 228 | t.Fatalf("Wrong type didn't yield the proper error: %v\n", err) 229 | } 230 | } 231 | 232 | func TestToTSTimeChain(t *testing.T) { 233 | src, dest := "1437743020", time.Time{} 234 | if err := NewParsingMap().ToTSTime("ts", &dest).Parse(mockForm{"ts": src}); err != nil { 235 | t.Fatal(err) 236 | } 237 | if expect, got := time.Unix(1437743020, 0), dest; expect.Sub(got) != 0 { 238 | t.Fatalf("Unexpected result.\nExpect:\t%s\nGot:\t%s\n", expect, got) 239 | } 240 | } 241 | 242 | func TestToTSTime(t *testing.T) { 243 | src, dest := "1437743020", time.Time{} 244 | if err := ToTSTime(src, &dest); err != nil { 245 | t.Fatal(err) 246 | } 247 | if expect, got := time.Unix(1437743020, 0), dest; expect.Sub(got) != 0 { 248 | t.Fatalf("Unexpected result.\nExpect:\t%s\nGot:\t%s\n", expect, got) 249 | } 250 | 251 | src, dest2 := "1437743020", &time.Time{} 252 | if err := ToTSTime(src, &dest2); err != nil { 253 | t.Fatal(err) 254 | } 255 | tt := time.Unix(1437743020, 0) 256 | if expect, got := &tt, dest2; expect.Sub(*got) != 0 { 257 | t.Fatalf("Unexpected result.\nExpect:\t%s\nGot:\t%s\n", expect, got) 258 | } 259 | 260 | // Error check 261 | if err := ToTSTime(src, dest); err != ErrWrongType { 262 | t.Fatalf("Wrong type didn't yield the proper error: %v\n", err) 263 | } 264 | src, dest = "abc", time.Time{} 265 | if err := ToTSTime(src, &dest); err == nil { 266 | t.Fatal("Invalid timestamp should yield an error") 267 | } 268 | } 269 | 270 | func TestToRFC3339TimeChain(t *testing.T) { 271 | src, dest := "2006-01-02T15:04:05Z", time.Time{} 272 | if err := NewParsingMap().ToRFC3339Time("date", &dest).Parse(mockForm{"date": src}); err != nil { 273 | t.Fatal(err) 274 | } 275 | tt, err := time.Parse(time.RFC3339, src) 276 | if err != nil { 277 | t.Fatal(err) 278 | } 279 | if expect, got := tt, dest; expect.Sub(got) != 0 { 280 | t.Fatalf("Unexpected result.\nExpect:\t%s\nGot:\t%s\n", expect, got) 281 | } 282 | } 283 | 284 | func TestToRFC3339Time(t *testing.T) { 285 | src, dest := "2006-01-02T15:04:05Z", time.Time{} 286 | if err := ToRFC3339Time(src, &dest); err != nil { 287 | t.Fatal(err) 288 | } 289 | tt, err := time.Parse(time.RFC3339, src) 290 | if err != nil { 291 | t.Fatal(err) 292 | } 293 | if expect, got := tt, dest; expect.Sub(got) != 0 { 294 | t.Fatalf("Unexpected result.\nExpect:\t%s\nGot:\t%s\n", expect, got) 295 | } 296 | 297 | src, dest2 := "2006-01-02T15:04:05Z", &time.Time{} 298 | if err := ToRFC3339Time(src, &dest2); err != nil { 299 | t.Fatal(err) 300 | } 301 | if expect, got := &tt, dest2; expect.Sub(*got) != 0 { 302 | t.Fatalf("Unexpected result.\nExpect:\t%s\nGot:\t%s\n", expect, got) 303 | } 304 | 305 | // Error check 306 | if err := ToRFC3339Time(src, dest); err != ErrWrongType { 307 | t.Fatalf("Wrong type didn't yield the proper error: %v\n", err) 308 | } 309 | src, dest = "abc", time.Time{} 310 | if err := ToRFC3339Time(src, &dest); err == nil { 311 | t.Fatal("Invalid timestamp should yield an error") 312 | } 313 | } 314 | --------------------------------------------------------------------------------