├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── bench_test.go ├── decode.go ├── decode_test.go ├── doc.go ├── doc_test.go ├── encode.go ├── encode_test.go ├── fields.go ├── fields_test.go ├── qstring.go ├── utils.go └── utils_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 | 26 | # Coverage reports 27 | coverage.* 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Dynamic Network Services Inc. 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all coverage style test 2 | 3 | all: test 4 | 5 | benchmark: 6 | go test -benchmem -bench=. 7 | 8 | coverage: 9 | go test -v -cover -coverprofile=coverage.out 10 | 11 | style: 12 | go vet 13 | 14 | test: style 15 | go test -v -cover 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # qstring 2 | This package provides an easy way to marshal and unmarshal url query string data to 3 | and from structs. 4 | 5 | ## Installation 6 | ```bash 7 | $ go get github.com/dyninc/qstring 8 | ``` 9 | 10 | ## Examples 11 | 12 | ### Unmarshaling 13 | 14 | ```go 15 | package main 16 | 17 | import ( 18 | "net/http" 19 | 20 | "github.com/dyninc/qstring" 21 | ) 22 | 23 | // Query is the http request query struct. 24 | type Query struct { 25 | Names []string 26 | Limit int 27 | Page int 28 | } 29 | 30 | func handler(w http.ResponseWriter, req *http.Request) { 31 | query := &Query{} 32 | err := qstring.Unmarshal(req.Url.Query(), query) 33 | if err != nil { 34 | http.Error(w, err.Error(), http.StatusBadRequest) 35 | } 36 | 37 | // ... run conditional logic based on provided query parameters 38 | } 39 | ``` 40 | 41 | The above example will unmarshal the query string from an http.Request and 42 | unmarshal it into the provided struct. This means that a query of 43 | `?names=foo&names=bar&limit=50&page=1` would be unmarshaled into a struct similar 44 | to the following: 45 | 46 | ```go 47 | Query{ 48 | Names: []string{"foo", "bar"}, 49 | Limit: 50, 50 | Page: 1, 51 | } 52 | ``` 53 | 54 | ### Marshalling 55 | `qstring` also exposes two methods of Marshaling structs *into* Query parameters, 56 | one will Marshal the provided struct into a raw query string, the other will 57 | Marshal a struct into a `url.Values` type. Some Examples of both follow. 58 | 59 | ### Marshal Raw Query String 60 | ```go 61 | package main 62 | 63 | import ( 64 | "fmt" 65 | "net/http" 66 | 67 | "github.com/dyninc/qstring" 68 | ) 69 | 70 | // Query is the http request query struct. 71 | type Query struct { 72 | Names []string 73 | Limit int 74 | Page int 75 | } 76 | 77 | func main() { 78 | query := &Query{ 79 | Names: []string{"foo", "bar"}, 80 | Limit: 50, 81 | Page: 1, 82 | } 83 | q, err := qstring.MarshalString(query) 84 | fmt.Println(q) 85 | // Output: names=foo&names=bar&limit=50&page=1 86 | } 87 | ``` 88 | 89 | ### Marshal url.Values 90 | ```go 91 | package main 92 | 93 | import ( 94 | "fmt" 95 | "net/http" 96 | 97 | "github.com/dyninc/qstring" 98 | ) 99 | 100 | // Query is the http request query struct. 101 | type Query struct { 102 | Names []string 103 | Limit int 104 | Page int 105 | } 106 | 107 | func main() { 108 | query := &Query{ 109 | Names: []string{"foo", "bar"}, 110 | Limit: 50, 111 | Page: 1, 112 | } 113 | q, err := qstring.Marshal(query) 114 | fmt.Println(q) 115 | // Output: map[names:[foo, bar] limit:[50] page:[1]] 116 | } 117 | ``` 118 | 119 | ### Nested 120 | In the same spirit as other Unmarshaling libraries, `qstring` allows you to 121 | Marshal/Unmarshal nested structs 122 | 123 | ```go 124 | package main 125 | 126 | import ( 127 | "net/http" 128 | 129 | "github.com/dyninc/qstring" 130 | ) 131 | 132 | // PagingParams represents common pagination information for query strings 133 | type PagingParams struct { 134 | Page int 135 | Limit int 136 | } 137 | 138 | // Query is the http request query struct. 139 | type Query struct { 140 | Names []string 141 | PageInfo PagingParams 142 | } 143 | ``` 144 | 145 | ### Complex Structures 146 | Again, in the spirit of other Unmarshaling libraries, `qstring` allows for some 147 | more complex types, such as pointers and time.Time fields. A more complete 148 | example might look something like the following code snippet 149 | 150 | ```go 151 | package main 152 | 153 | import ( 154 | "time" 155 | ) 156 | 157 | // PagingParams represents common pagination information for query strings 158 | type PagingParams struct { 159 | Page int `qstring:"page"` 160 | Limit int `qstring:"limit"` 161 | } 162 | 163 | // Query is the http request query struct. 164 | type Query struct { 165 | Names []string 166 | IDs []int 167 | PageInfo *PagingParams 168 | Created time.Time 169 | Modified time.Time 170 | } 171 | ``` 172 | 173 | ## Additional Notes 174 | * All Timestamps are assumed to be in RFC3339 format 175 | * A struct field tag of `qstring` is supported and supports all of the features 176 | you've come to know and love from Go (un)marshalers. 177 | * A field tag with a value of `qstring:"-"` instructs `qstring` to ignore the field. 178 | * A field tag with an the `omitempty` option set will be ignored if the field 179 | being marshaled has a zero value. `qstring:"name,omitempty"` 180 | 181 | ### Custom Fields 182 | In order to facilitate more complex queries `qstring` also provides some custom 183 | fields to save you a bit of headache with custom marshal/unmarshaling logic. 184 | Currently the following custom fields are provided: 185 | 186 | * `qstring.ComparativeTime` - Supports timestamp query parameters with optional 187 | logical operators (<, >, <=, >=) such as `?created<=2006-01-02T15:04:05Z` 188 | 189 | 190 | ## Benchmarks 191 | ``` 192 | BenchmarkUnmarshall-4 500000 2711 ns/op 448 B/op 23 allocs/op 193 | BenchmarkRawPLiteral-4 1000000 1675 ns/op 448 B/op 23 allocs/op 194 | ok github.com/dyninc/qstring 3.163s 195 | ``` 196 | -------------------------------------------------------------------------------- /bench_test.go: -------------------------------------------------------------------------------- 1 | package qstring 2 | 3 | import ( 4 | "net/url" 5 | "testing" 6 | ) 7 | 8 | // Straight benchmark literal. 9 | func BenchmarkUnmarshall(b *testing.B) { 10 | query := url.Values{ 11 | "limit": []string{"10"}, 12 | "page": []string{"1"}, 13 | "fields": []string{"a", "b", "c"}, 14 | } 15 | type QueryStruct struct { 16 | Fields []string 17 | Limit int 18 | Page int 19 | } 20 | b.ResetTimer() 21 | for i := 0; i < b.N; i++ { 22 | data := &QueryStruct{} 23 | err := Unmarshal(query, data) 24 | if err != nil { 25 | b.Fatal(err) 26 | } 27 | } 28 | } 29 | 30 | // Parallel benchmark literal. 31 | func BenchmarkRawPLiteral(b *testing.B) { 32 | query := url.Values{ 33 | "limit": []string{"10"}, 34 | "page": []string{"1"}, 35 | "fields": []string{"a", "b", "c"}, 36 | } 37 | type QueryStruct struct { 38 | Fields []string 39 | Limit int 40 | Page int 41 | } 42 | b.ResetTimer() 43 | b.RunParallel(func(pb *testing.PB) { 44 | for pb.Next() { 45 | data := &QueryStruct{} 46 | err := Unmarshal(query, data) 47 | if err != nil { 48 | b.Fatal(err) 49 | } 50 | } 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /decode.go: -------------------------------------------------------------------------------- 1 | package qstring 2 | 3 | import ( 4 | "net/url" 5 | "reflect" 6 | "strconv" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | // Unmarshaller defines the interface for performing custom unmarshalling of 12 | // query strings into struct values 13 | type Unmarshaller interface { 14 | UnmarshalQuery(url.Values) error 15 | } 16 | 17 | // Unmarshal unmarshalls the provided url.Values (query string) into the 18 | // interface provided 19 | func Unmarshal(data url.Values, v interface{}) error { 20 | var d decoder 21 | d.init(data) 22 | return d.unmarshal(v) 23 | } 24 | 25 | // An InvalidUnmarshalError describes an invalid argument passed to Unmarshal. 26 | // (The argument to Unmarshal must be a non-nil pointer.) 27 | type InvalidUnmarshalError struct { 28 | Type reflect.Type 29 | } 30 | 31 | func (e InvalidUnmarshalError) Error() string { 32 | if e.Type == nil { 33 | return "qstring: Unmarshal(nil)" 34 | } 35 | 36 | if e.Type.Kind() != reflect.Ptr { 37 | return "qstring: Unmarshal(non-pointer " + e.Type.String() + ")" 38 | } 39 | return "qstring: Unmarshal(nil " + e.Type.String() + ")" 40 | } 41 | 42 | type decoder struct { 43 | data url.Values 44 | } 45 | 46 | func (d *decoder) init(data url.Values) *decoder { 47 | d.data = data 48 | return d 49 | } 50 | 51 | func (d *decoder) unmarshal(v interface{}) error { 52 | rv := reflect.ValueOf(v) 53 | if rv.Kind() != reflect.Ptr || rv.IsNil() { 54 | return &InvalidUnmarshalError{reflect.TypeOf(v)} 55 | } 56 | 57 | switch val := v.(type) { 58 | case Unmarshaller: 59 | return val.UnmarshalQuery(d.data) 60 | default: 61 | return d.value(rv) 62 | } 63 | } 64 | 65 | func (d *decoder) value(val reflect.Value) error { 66 | var err error 67 | elem := val.Elem() 68 | typ := elem.Type() 69 | 70 | for i := 0; i < elem.NumField(); i++ { 71 | // pull out the qstring struct tag 72 | elemField := elem.Field(i) 73 | typField := typ.Field(i) 74 | qstring, _ := parseTag(typField.Tag.Get(Tag)) 75 | if qstring == "" { 76 | // resolvable fields must have at least the `flag` struct tag 77 | qstring = strings.ToLower(typField.Name) 78 | } 79 | 80 | // determine if this is an unsettable field or was explicitly set to be 81 | // ignored 82 | if !elemField.CanSet() || qstring == "-" { 83 | continue 84 | } 85 | 86 | // only do work if the current fields query string parameter was provided 87 | if query, ok := d.data[qstring]; ok { 88 | switch k := typField.Type.Kind(); k { 89 | case reflect.Slice: 90 | err = d.coerceSlice(query, k, elemField) 91 | default: 92 | err = d.coerce(query[0], k, elemField) 93 | } 94 | } else if typField.Type.Kind() == reflect.Struct { 95 | if elemField.CanAddr() { 96 | err = d.value(elemField.Addr()) 97 | } 98 | } 99 | if err != nil { 100 | return err 101 | } 102 | } 103 | return nil 104 | } 105 | 106 | // coerce converts the provided query parameter slice into the proper type for 107 | // the target field. this coerced value is then assigned to the current field 108 | func (d *decoder) coerce(query string, target reflect.Kind, field reflect.Value) error { 109 | var err error 110 | var c interface{} 111 | 112 | switch target { 113 | case reflect.String: 114 | field.SetString(query) 115 | case reflect.Bool: 116 | c, err = strconv.ParseBool(query) 117 | if err == nil { 118 | field.SetBool(c.(bool)) 119 | } 120 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 121 | c, err = strconv.ParseInt(query, 10, 64) 122 | if err == nil { 123 | field.SetInt(c.(int64)) 124 | } 125 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 126 | c, err = strconv.ParseUint(query, 10, 64) 127 | if err == nil { 128 | field.SetUint(c.(uint64)) 129 | } 130 | case reflect.Float32, reflect.Float64: 131 | c, err = strconv.ParseFloat(query, 64) 132 | if err == nil { 133 | field.SetFloat(c.(float64)) 134 | } 135 | case reflect.Struct: 136 | // unescape the query parameter before attempting to parse it 137 | query, err = url.QueryUnescape(query) 138 | if err != nil { 139 | return err 140 | } 141 | 142 | switch field.Interface().(type) { 143 | case time.Time: 144 | var t time.Time 145 | t, err = time.Parse(time.RFC3339, query) 146 | if err == nil { 147 | field.Set(reflect.ValueOf(t)) 148 | } 149 | case ComparativeTime: 150 | t := *NewComparativeTime() 151 | err = t.Parse(query) 152 | if err == nil { 153 | field.Set(reflect.ValueOf(t)) 154 | } 155 | default: 156 | d.value(field) 157 | } 158 | } 159 | 160 | return err 161 | } 162 | 163 | // coerceSlice creates a new slice of the appropriate type for the target field 164 | // and coerces each of the query parameter values into the destination type. 165 | // Should any of the provided query parameters fail to be coerced, an error is 166 | // returned and the entire slice will not be applied 167 | func (d *decoder) coerceSlice(query []string, target reflect.Kind, field reflect.Value) error { 168 | var err error 169 | sliceType := field.Type().Elem() 170 | coerceKind := sliceType.Kind() 171 | sl := reflect.MakeSlice(reflect.SliceOf(sliceType), 0, 0) 172 | // Create a pointer to a slice value and set it to the slice 173 | slice := reflect.New(sl.Type()) 174 | slice.Elem().Set(sl) 175 | for _, q := range query { 176 | val := reflect.New(sliceType).Elem() 177 | err = d.coerce(q, coerceKind, val) 178 | if err != nil { 179 | return err 180 | } 181 | slice.Elem().Set(reflect.Append(slice.Elem(), val)) 182 | } 183 | field.Set(slice.Elem()) 184 | return nil 185 | } 186 | -------------------------------------------------------------------------------- /decode_test.go: -------------------------------------------------------------------------------- 1 | package qstring 2 | 3 | import ( 4 | "errors" 5 | "net/url" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | type TestStruct struct { 11 | Name string `qstring:"name"` 12 | Do bool 13 | 14 | // int fields 15 | Page int `qstring:"page"` 16 | ID int8 17 | Small int16 18 | Med int32 19 | Big int64 20 | 21 | // uint fields 22 | UPage uint 23 | UID uint8 24 | USmall uint16 25 | UMed uint32 26 | UBig uint64 27 | 28 | // Floats 29 | Float32 float32 30 | Float64 float64 31 | 32 | // slice fields 33 | Fields []string `qstring:"fields"` 34 | DoFields []bool `qstring:"dofields"` 35 | Counts []int 36 | IDs []int8 37 | Smalls []int16 38 | Meds []int32 39 | Bigs []int64 40 | 41 | // uint fields 42 | UPages []uint 43 | UIDs []uint8 44 | USmalls []uint16 45 | UMeds []uint32 46 | UBigs []uint64 47 | 48 | // Floats 49 | Float32s []float32 50 | Float64s []float64 51 | hidden int 52 | Hidden int `qstring:"-"` 53 | } 54 | 55 | func TestUnmarshall(t *testing.T) { 56 | var ts TestStruct 57 | query := url.Values{ 58 | "name": []string{"SomeName"}, 59 | "do": []string{"true"}, 60 | "page": []string{"1"}, 61 | "id": []string{"12"}, 62 | "small": []string{"13"}, 63 | "med": []string{"14"}, 64 | "big": []string{"15"}, 65 | "upage": []string{"2"}, 66 | "uid": []string{"16"}, 67 | "usmall": []string{"17"}, 68 | "umed": []string{"18"}, 69 | "ubig": []string{"19"}, 70 | "float32": []string{"6000"}, 71 | "float64": []string{"7000"}, 72 | "fields": []string{"foo", "bar"}, 73 | "dofields": []string{"true", "false"}, 74 | "counts": []string{"1", "2"}, 75 | "ids": []string{"3", "4", "5"}, 76 | "smalls": []string{"6", "7", "8"}, 77 | "meds": []string{"9", "10", "11"}, 78 | "bigs": []string{"12", "13", "14"}, 79 | "upages": []string{"2", "3", "4"}, 80 | "uids": []string{"5", "6", "7"}, 81 | "usmalls": []string{"8", "9", "10"}, 82 | "umeds": []string{"9", "10", "11"}, 83 | "ubigs": []string{"12", "13", "14"}, 84 | "float32s": []string{"6000", "6001", "6002"}, 85 | "float64s": []string{"7000", "7001", "7002"}, 86 | } 87 | 88 | err := Unmarshal(query, &ts) 89 | if err != nil { 90 | t.Fatal(err.Error()) 91 | } 92 | 93 | if ts.Page != 1 { 94 | t.Errorf("Expected page to be 1, got %d", ts.Page) 95 | } 96 | 97 | if len(ts.Fields) != 2 { 98 | t.Errorf("Expected 2 fields, got %d", len(ts.Fields)) 99 | } 100 | } 101 | 102 | func TestUnmarshalNested(t *testing.T) { 103 | type Paging struct { 104 | Page int 105 | Limit int 106 | } 107 | 108 | type Params struct { 109 | Paging Paging 110 | Name string 111 | } 112 | 113 | query := url.Values{ 114 | "name": []string{"SomeName"}, 115 | "page": []string{"1"}, 116 | "limit": []string{"50"}, 117 | } 118 | 119 | params := &Params{} 120 | 121 | err := Unmarshal(query, params) 122 | if err != nil { 123 | t.Fatal(err.Error()) 124 | } 125 | 126 | if params.Paging.Page != 1 { 127 | t.Errorf("Nested Struct Failed to Unmarshal. Expected 1, got %d", params.Paging.Page) 128 | } 129 | } 130 | 131 | func TestUnmarshalTime(t *testing.T) { 132 | type Query struct { 133 | Created time.Time 134 | LastUpdated time.Time 135 | } 136 | 137 | createdTS := "2006-01-02T15:04:05Z" 138 | updatedTS := "2016-01-02T15:04:05-07:00" 139 | 140 | query := url.Values{ 141 | "created": []string{createdTS}, 142 | "lastupdated": []string{updatedTS}, 143 | } 144 | 145 | params := &Query{} 146 | err := Unmarshal(query, params) 147 | if err != nil { 148 | t.Fatal(err.Error()) 149 | } 150 | 151 | if params.Created.Format(time.RFC3339) != createdTS { 152 | t.Errorf("Expected created ts of %s, got %s instead.", createdTS, params.Created.Format(time.RFC3339)) 153 | } 154 | 155 | if params.LastUpdated.Format(time.RFC3339) != updatedTS { 156 | t.Errorf("Expected update ts of %s, got %s instead.", updatedTS, params.LastUpdated.Format(time.RFC3339)) 157 | } 158 | } 159 | 160 | func TestUnmarshalInvalidTypes(t *testing.T) { 161 | var err error 162 | var ts *TestStruct 163 | testio := []struct { 164 | inp interface{} 165 | errString string 166 | }{ 167 | {inp: nil, errString: "qstring: Unmarshal(nil)"}, 168 | {inp: TestStruct{}, errString: "qstring: Unmarshal(non-pointer qstring.TestStruct)"}, 169 | {inp: ts, errString: "qstring: Unmarshal(nil *qstring.TestStruct)"}, 170 | } 171 | 172 | for _, test := range testio { 173 | err = Unmarshal(url.Values{}, test.inp) 174 | if err == nil { 175 | t.Errorf("Expected invalid type error, got success instead") 176 | } 177 | 178 | if err.Error() != test.errString { 179 | t.Errorf("Got %q error, expected %q", err.Error(), test.errString) 180 | } 181 | } 182 | } 183 | 184 | var errNoNames = errors.New("No Names Provided") 185 | 186 | type MarshalInterfaceTest struct { 187 | Names []string 188 | } 189 | 190 | func (u *MarshalInterfaceTest) UnmarshalQuery(v url.Values) error { 191 | var ok bool 192 | if u.Names, ok = v["names"]; ok { 193 | return nil 194 | } 195 | return errNoNames 196 | } 197 | 198 | func TestUnmarshaller(t *testing.T) { 199 | testIO := []struct { 200 | inp url.Values 201 | expected interface{} 202 | }{ 203 | {url.Values{"names": []string{"foo", "bar"}}, nil}, 204 | {make(url.Values), errNoNames}, 205 | } 206 | 207 | s := &MarshalInterfaceTest{Names: []string{}} 208 | for _, test := range testIO { 209 | err := Unmarshal(test.inp, s) 210 | if err != test.expected { 211 | t.Errorf("Expected Unmarshaller to return %s, but got %s instead", test.expected, err) 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package qstring provides an an easy way to marshal and unmarshal query string 2 | // data to and from structs representations. 3 | // 4 | // This library was designed with consistency in mind, to this end the provided 5 | // Marshal and Unmarshal interfaces should seem familiar to those who have used 6 | // packages such as "encoding/json". 7 | // 8 | // Additionally this library includes support for converting query parameters 9 | // to and from time.Time types as well as nested structs and pointer values. 10 | package qstring 11 | -------------------------------------------------------------------------------- /doc_test.go: -------------------------------------------------------------------------------- 1 | package qstring_test 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "os" 7 | "time" 8 | 9 | "github.com/dyninc/qstring" 10 | ) 11 | 12 | func ExampleUnmarshal() { 13 | // Query is the http request query struct. 14 | type Query struct { 15 | Names []string 16 | Limit int 17 | Page int 18 | } 19 | 20 | query := &Query{} 21 | qValues, _ := url.ParseQuery("names=foo&names=bar&limit=50&page=1") 22 | err := qstring.Unmarshal(qValues, query) 23 | if err != nil { 24 | panic("Unable to Parse Query String") 25 | } 26 | 27 | os.Stdout.Write([]byte(fmt.Sprintf("%+v", query))) 28 | // Output: &{Names:[foo bar] Limit:50 Page:1} 29 | } 30 | 31 | func ExampleMarshalString() { 32 | // Query is the http request query struct. 33 | type Query struct { 34 | Names []string 35 | Limit int 36 | Page int 37 | } 38 | 39 | query := &Query{ 40 | Names: []string{"foo", "bar"}, 41 | Limit: 50, 42 | Page: 1, 43 | } 44 | q, _ := qstring.MarshalString(query) 45 | os.Stdout.Write([]byte(q)) 46 | // Output: limit=50&names=foo&names=bar&page=1 47 | } 48 | 49 | func ExampleUnmarshal_complex() { 50 | // PagingParams represents common pagination information for query strings 51 | type PagingParams struct { 52 | Page int `qstring:"page"` 53 | Limit int `qstring:"limit"` 54 | } 55 | 56 | // Query is the http request query struct. 57 | type Query struct { 58 | Names []string 59 | IDs []int 60 | PageInfo *PagingParams 61 | Created time.Time 62 | } 63 | query := &Query{} 64 | qValues, _ := url.ParseQuery("names=foo&names=bar&limit=50&page=1&ids=1&ids=2&created=2006-01-02T15:04:05Z") 65 | err := qstring.Unmarshal(qValues, query) 66 | if err != nil { 67 | panic("Unable to Parse Query String") 68 | } 69 | } 70 | 71 | func ExampleComparativeTime() { 72 | type DateQuery struct { 73 | Created qstring.ComparativeTime 74 | Modified qstring.ComparativeTime 75 | } 76 | 77 | var query DateQuery 78 | qValues, _ := url.ParseQuery("created=>=2006-01-02T15:04:05Z&modified=<=2016-01-01T15:04Z") 79 | err := qstring.Unmarshal(qValues, &query) 80 | if err != nil { 81 | panic("Unable to Parse Query String") 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /encode.go: -------------------------------------------------------------------------------- 1 | package qstring 2 | 3 | import ( 4 | "net/url" 5 | "reflect" 6 | "strconv" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | // Marshaller defines the interface for performing custom marshalling of struct 12 | // values into query strings 13 | type Marshaller interface { 14 | MarshalQuery() (url.Values, error) 15 | } 16 | 17 | // Marshal marshals the provided struct into a url.Values collection 18 | func Marshal(v interface{}) (url.Values, error) { 19 | var e encoder 20 | e.init(v) 21 | return e.marshal() 22 | } 23 | 24 | // Marshal marshals the provided struct into a raw query string and returns a 25 | // conditional error 26 | func MarshalString(v interface{}) (string, error) { 27 | vals, err := Marshal(v) 28 | if err != nil { 29 | return "", err 30 | } 31 | return vals.Encode(), nil 32 | } 33 | 34 | // An InvalidMarshalError describes an invalid argument passed to Marshal or 35 | // MarshalValue. (The argument to Marshal must be a non-nil pointer.) 36 | type InvalidMarshalError struct { 37 | Type reflect.Type 38 | } 39 | 40 | func (e InvalidMarshalError) Error() string { 41 | if e.Type == nil { 42 | return "qstring: MarshalString(nil)" 43 | } 44 | 45 | if e.Type.Kind() != reflect.Ptr { 46 | return "qstring: MarshalString(non-pointer " + e.Type.String() + ")" 47 | } 48 | return "qstring: MarshalString(nil " + e.Type.String() + ")" 49 | } 50 | 51 | type encoder struct { 52 | data interface{} 53 | } 54 | 55 | func (e *encoder) init(v interface{}) *encoder { 56 | e.data = v 57 | return e 58 | } 59 | 60 | func (e *encoder) marshal() (url.Values, error) { 61 | rv := reflect.ValueOf(e.data) 62 | if rv.Kind() != reflect.Ptr || rv.IsNil() { 63 | return nil, &InvalidMarshalError{reflect.TypeOf(e.data)} 64 | } 65 | 66 | switch val := e.data.(type) { 67 | case Marshaller: 68 | return val.MarshalQuery() 69 | default: 70 | return e.value(rv) 71 | } 72 | } 73 | 74 | func (e *encoder) value(val reflect.Value) (url.Values, error) { 75 | elem := val.Elem() 76 | typ := elem.Type() 77 | 78 | var err error 79 | var output = make(url.Values) 80 | for i := 0; i < elem.NumField(); i++ { 81 | // pull out the qstring struct tag 82 | elemField := elem.Field(i) 83 | typField := typ.Field(i) 84 | qstring, omit := parseTag(typField.Tag.Get(Tag)) 85 | if qstring == "" { 86 | // resolvable fields must have at least the `flag` struct tag 87 | qstring = strings.ToLower(typField.Name) 88 | } 89 | 90 | // determine if this is an unsettable field or was explicitly set to be 91 | // ignored 92 | if !elemField.CanSet() || qstring == "-" || (omit && isEmptyValue(elemField)) { 93 | continue 94 | } 95 | 96 | // only do work if the current fields query string parameter was provided 97 | switch k := typField.Type.Kind(); k { 98 | default: 99 | output.Set(qstring, marshalValue(elemField, k)) 100 | case reflect.Slice: 101 | output[qstring] = marshalSlice(elemField) 102 | case reflect.Ptr: 103 | marshalStruct(output, qstring, reflect.Indirect(elemField), k) 104 | case reflect.Struct: 105 | marshalStruct(output, qstring, elemField, k) 106 | } 107 | } 108 | return output, err 109 | } 110 | 111 | func marshalSlice(field reflect.Value) []string { 112 | var out []string 113 | for i := 0; i < field.Len(); i++ { 114 | out = append(out, marshalValue(field.Index(i), field.Index(i).Kind())) 115 | } 116 | return out 117 | } 118 | 119 | func marshalValue(field reflect.Value, source reflect.Kind) string { 120 | switch source { 121 | case reflect.String: 122 | return field.String() 123 | case reflect.Bool: 124 | return strconv.FormatBool(field.Bool()) 125 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 126 | return strconv.FormatInt(field.Int(), 10) 127 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 128 | return strconv.FormatUint(field.Uint(), 10) 129 | case reflect.Float32, reflect.Float64: 130 | return strconv.FormatFloat(field.Float(), 'G', -1, 64) 131 | case reflect.Struct: 132 | switch field.Interface().(type) { 133 | case time.Time: 134 | return field.Interface().(time.Time).Format(time.RFC3339) 135 | case ComparativeTime: 136 | return field.Interface().(ComparativeTime).String() 137 | } 138 | } 139 | return "" 140 | } 141 | 142 | func marshalStruct(output url.Values, qstring string, field reflect.Value, source reflect.Kind) error { 143 | var err error 144 | switch field.Interface().(type) { 145 | case time.Time, ComparativeTime: 146 | output.Set(qstring, marshalValue(field, source)) 147 | default: 148 | var vals url.Values 149 | if field.CanAddr() { 150 | vals, err = Marshal(field.Addr().Interface()) 151 | } 152 | 153 | if err != nil { 154 | return err 155 | } 156 | for key, list := range vals { 157 | output[key] = list 158 | } 159 | } 160 | return nil 161 | } 162 | -------------------------------------------------------------------------------- /encode_test.go: -------------------------------------------------------------------------------- 1 | package qstring 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestMarshallString(t *testing.T) { 11 | ts := TestStruct{ 12 | Name: "SomeName", 13 | Do: true, 14 | Page: 1, 15 | ID: 12, 16 | Small: 13, 17 | Med: 14, 18 | Big: 15, 19 | UPage: 2, 20 | UID: 16, 21 | USmall: 17, 22 | UMed: 17, 23 | UBig: 17, 24 | Float32: 6000, 25 | Float64: 7000, 26 | Fields: []string{"foo", "bar"}, 27 | DoFields: []bool{true, false}, 28 | Counts: []int{1, 2}, 29 | IDs: []int8{3, 4}, 30 | Smalls: []int16{6, 7}, 31 | Meds: []int32{9, 10}, 32 | Bigs: []int64{12, 13}, 33 | UPages: []uint{2, 3}, 34 | UIDs: []uint8{5, 6}, 35 | USmalls: []uint16{8, 9}, 36 | UMeds: []uint32{9, 10}, 37 | UBigs: []uint64{12, 13}, 38 | Float32s: []float32{6000, 6001}, 39 | Float64s: []float64{7000, 7001}, 40 | } 41 | 42 | expected := []string{"name=SomeName", "do=true", "page=1", "id=12", "small=13", 43 | "med=14", "big=15", "upage=2", "uid=16", "usmall=17", "umed=17", "ubig=17", 44 | "float32=6000", "float64=7000", "fields=foo", "fields=bar", "dofields=true", 45 | "dofields=false", "counts=1", "counts=2", "ids=3", "ids=4", "smalls=6", 46 | "smalls=7", "meds=9", "meds=10", "bigs=12", "bigs=13", "upages=2", 47 | "upages=3", "uids=5", "uids=6", "usmalls=8", "usmalls=9", "umeds=9", 48 | "umeds=10", "ubigs=12", "ubigs=13", "float32s=6000", "float32s=6001", 49 | "float64s=7000", "float64s=7001"} 50 | query, err := MarshalString(&ts) 51 | if err != nil { 52 | t.Fatal(err.Error()) 53 | } 54 | 55 | for _, param := range expected { 56 | if !strings.Contains(query, param) { 57 | t.Errorf("Expected %s to contain %s", query, param) 58 | } 59 | } 60 | } 61 | 62 | func TestMarshallValues(t *testing.T) { 63 | ts := TestStruct{ 64 | Name: "SomeName", 65 | Do: true, 66 | Page: 1, 67 | ID: 12, 68 | Small: 13, 69 | Med: 14, 70 | Big: 15, 71 | UPage: 2, 72 | UID: 16, 73 | USmall: 17, 74 | UMed: 17, 75 | UBig: 17, 76 | Float32: 6000, 77 | Float64: 7000, 78 | Fields: []string{"foo", "bar"}, 79 | DoFields: []bool{true, false}, 80 | Counts: []int{1, 2}, 81 | IDs: []int8{3, 4}, 82 | Smalls: []int16{6, 7}, 83 | Meds: []int32{9, 10}, 84 | Bigs: []int64{12, 13}, 85 | UPages: []uint{2, 3}, 86 | UIDs: []uint8{5, 6}, 87 | USmalls: []uint16{8, 9}, 88 | UMeds: []uint32{9, 10}, 89 | UBigs: []uint64{12, 13}, 90 | Float32s: []float32{6000, 6001}, 91 | Float64s: []float64{7000, 7001}, 92 | } 93 | 94 | expected := url.Values{ 95 | "name": []string{"SomeName"}, 96 | "do": []string{"true"}, 97 | "page": []string{"1"}, 98 | "id": []string{"12"}, 99 | "small": []string{"13"}, 100 | "med": []string{"14"}, 101 | "big": []string{"15"}, 102 | "upage": []string{"2"}, 103 | "uid": []string{"16"}, 104 | "usmall": []string{"17"}, 105 | "umed": []string{"18"}, 106 | "ubig": []string{"19"}, 107 | "float32": []string{"6000"}, 108 | "float64": []string{"7000"}, 109 | "fields": []string{"foo", "bar"}, 110 | "dofields": []string{"true", "false"}, 111 | "counts": []string{"1", "2"}, 112 | "ids": []string{"3", "4", "5"}, 113 | "smalls": []string{"6", "7", "8"}, 114 | "meds": []string{"9", "10", "11"}, 115 | "bigs": []string{"12", "13", "14"}, 116 | "upages": []string{"2", "3", "4"}, 117 | "uids": []string{"5, 6, 7"}, 118 | "usmalls": []string{"8", "9", "10"}, 119 | "umeds": []string{"9", "10", "11"}, 120 | "ubigs": []string{"12", "13", "14"}, 121 | "float32s": []string{"6000", "6001", "6002"}, 122 | "float64s": []string{"7000", "7001", "7002"}, 123 | } 124 | values, err := Marshal(&ts) 125 | if err != nil { 126 | t.Fatal(err.Error()) 127 | } 128 | 129 | if len(values) != len(expected) { 130 | t.Errorf("Expected %d fields, got %d. Hidden is %q", 131 | len(expected), len(values), values["hidden"]) 132 | } 133 | } 134 | 135 | func TestInvalidMarshalString(t *testing.T) { 136 | var err error 137 | var ts *TestStruct 138 | testio := []struct { 139 | inp interface{} 140 | errString string 141 | }{ 142 | {inp: nil, errString: "qstring: MarshalString(nil)"}, 143 | {inp: TestStruct{}, errString: "qstring: MarshalString(non-pointer qstring.TestStruct)"}, 144 | {inp: ts, errString: "qstring: MarshalString(nil *qstring.TestStruct)"}, 145 | } 146 | 147 | for _, test := range testio { 148 | _, err = MarshalString(test.inp) 149 | if err == nil { 150 | t.Errorf("Expected invalid type error, got success instead") 151 | } 152 | 153 | if err.Error() != test.errString { 154 | t.Errorf("Got %q error, expected %q", err.Error(), test.errString) 155 | } 156 | } 157 | } 158 | 159 | func TestMarshalTime(t *testing.T) { 160 | type Query struct { 161 | Created time.Time 162 | LastUpdated time.Time 163 | } 164 | 165 | createdTS := "2006-01-02T15:04:05Z" 166 | createdTime, _ := time.Parse(time.RFC3339, createdTS) 167 | updatedTS := "2016-01-02T15:04:05-07:00" 168 | updatedTime, _ := time.Parse(time.RFC3339, updatedTS) 169 | 170 | q := &Query{Created: createdTime, LastUpdated: updatedTime} 171 | result, err := MarshalString(q) 172 | if err != nil { 173 | t.Fatalf("Unable to marshal timestamp: %s", err.Error()) 174 | } 175 | 176 | var unescaped string 177 | unescaped, err = url.QueryUnescape(result) 178 | if err != nil { 179 | t.Fatalf("Unable to unescape query string %q: %q", result, err.Error()) 180 | } 181 | 182 | expected := []string{"created=2006-01-02T15:04:05Z", 183 | "lastupdated=2016-01-02T15:04:05-07:00"} 184 | for _, ts := range expected { 185 | if !strings.Contains(unescaped, ts) { 186 | t.Errorf("Expected query string %s to contain %s", unescaped, ts) 187 | } 188 | } 189 | } 190 | 191 | func TestMarshalNested(t *testing.T) { 192 | type Paging struct { 193 | Page int 194 | Limit int 195 | } 196 | 197 | type Params struct { 198 | Paging Paging 199 | Name string 200 | } 201 | 202 | params := &Params{Name: "SomeName", 203 | Paging: Paging{Page: 1, Limit: 50}, 204 | } 205 | 206 | result, err := MarshalString(params) 207 | if err != nil { 208 | t.Fatalf("Unable to marshal nested struct: %s", err.Error()) 209 | } 210 | 211 | var unescaped string 212 | unescaped, err = url.QueryUnescape(result) 213 | if err != nil { 214 | t.Fatalf("Unable to unescape query string %q: %q", result, err.Error()) 215 | } 216 | 217 | // ensure the nested struct isn't iteself included in the query string 218 | if strings.Contains(unescaped, "paging=") { 219 | t.Errorf("Nested struct was included in %q", unescaped) 220 | } 221 | 222 | // ensure fields we expect to be present are 223 | expected := []string{"name=SomeName", "page=1", "limit=50"} 224 | for _, q := range expected { 225 | if !strings.Contains(unescaped, q) { 226 | t.Errorf("Expected query string %s to contain %s", unescaped, q) 227 | } 228 | } 229 | } 230 | 231 | func TestMarshalNestedPtrs(t *testing.T) { 232 | type Paging struct { 233 | Page int 234 | Limit int 235 | } 236 | 237 | type Params struct { 238 | Paging *Paging 239 | Name string 240 | } 241 | 242 | params := &Params{Name: "SomeName", 243 | Paging: &Paging{Page: 1, Limit: 50}, 244 | } 245 | 246 | result, err := MarshalString(params) 247 | if err != nil { 248 | t.Fatalf("Unable to marshal nested struct: %s", err.Error()) 249 | } 250 | 251 | var unescaped string 252 | unescaped, err = url.QueryUnescape(result) 253 | if err != nil { 254 | t.Fatalf("Unable to unescape query string %q: %q", result, err.Error()) 255 | } 256 | 257 | // ensure the nested struct isn't iteself included in the query string 258 | if strings.Contains(unescaped, "paging=") { 259 | t.Errorf("Nested struct was included in %q", unescaped) 260 | } 261 | 262 | // ensure fields we expect to be present are 263 | expected := []string{"name=SomeName", "page=1", "limit=50"} 264 | for _, q := range expected { 265 | if !strings.Contains(unescaped, q) { 266 | t.Errorf("Expected query string %s to contain %s", unescaped, q) 267 | } 268 | } 269 | } 270 | 271 | func (u *MarshalInterfaceTest) MarshalQuery() (url.Values, error) { 272 | return url.Values{"names": u.Names}, nil 273 | } 274 | 275 | func TestMarshaller(t *testing.T) { 276 | s := &MarshalInterfaceTest{Names: []string{"foo", "bar"}} 277 | 278 | testIO := []struct { 279 | inp *MarshalInterfaceTest 280 | vals url.Values 281 | err error 282 | }{ 283 | {s, url.Values{"names": s.Names}, nil}, 284 | } 285 | 286 | for _, test := range testIO { 287 | v, err := Marshal(test.inp) 288 | if err != test.err { 289 | t.Errorf("Expected Marshaller to return %s, but got %s instead", test.err, err) 290 | } 291 | 292 | if len(v) != len(test.vals) { 293 | t.Errorf("Expected %q, got %q instead", test.vals, v) 294 | } 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /fields.go: -------------------------------------------------------------------------------- 1 | package qstring 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | // parseOperator parses a leading logical operator out of the provided string 11 | func parseOperator(s string) string { 12 | switch s[0] { 13 | case 60: // "<" 14 | switch s[1] { 15 | case 61: // "=" 16 | return "<=" 17 | default: 18 | return "<" 19 | } 20 | case 62: // ">" 21 | switch s[1] { 22 | case 61: // "=" 23 | return ">=" 24 | default: 25 | return ">" 26 | } 27 | default: 28 | // no operator found, default to "=" 29 | return "=" 30 | } 31 | } 32 | 33 | // ComparativeTime is a field that can be used for specifying a query parameter 34 | // which includes a conditional operator and a timestamp 35 | type ComparativeTime struct { 36 | Operator string 37 | Time time.Time 38 | } 39 | 40 | // NewComparativeTime returns a new ComparativeTime instance with a default 41 | // operator of "=" 42 | func NewComparativeTime() *ComparativeTime { 43 | return &ComparativeTime{Operator: "="} 44 | } 45 | 46 | // Parse is used to parse a query string into a ComparativeTime instance 47 | func (c *ComparativeTime) Parse(query string) error { 48 | if len(query) <= 2 { 49 | return errors.New("qstring: Invalid Timestamp Query") 50 | } 51 | 52 | c.Operator = parseOperator(query) 53 | 54 | // if no operator was provided and we defaulted to an equality operator 55 | if !strings.HasPrefix(query, c.Operator) { 56 | query = fmt.Sprintf("=%s", query) 57 | } 58 | 59 | var err error 60 | c.Time, err = time.Parse(time.RFC3339, query[len(c.Operator):]) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | return nil 66 | } 67 | 68 | // String returns this ComparativeTime instance in the form of the query 69 | // parameter that it came in on 70 | func (c ComparativeTime) String() string { 71 | return fmt.Sprintf("%s%s", c.Operator, c.Time.Format(time.RFC3339)) 72 | } 73 | -------------------------------------------------------------------------------- /fields_test.go: -------------------------------------------------------------------------------- 1 | package qstring 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestComparativeTimeParse(t *testing.T) { 10 | tme := "2006-01-02T15:04:05Z" 11 | testio := []struct { 12 | inp string 13 | operator string 14 | errString string 15 | }{ 16 | {inp: tme, operator: "=", errString: ""}, 17 | {inp: ">" + tme, operator: ">", errString: ""}, 18 | {inp: "<" + tme, operator: "<", errString: ""}, 19 | {inp: ">=" + tme, operator: ">=", errString: ""}, 20 | {inp: "<=" + tme, operator: "<=", errString: ""}, 21 | {inp: "<=" + tme, operator: "<=", errString: ""}, 22 | {inp: "", operator: "=", errString: "qstring: Invalid Timestamp Query"}, 23 | {inp: ">=", operator: "=", errString: "qstring: Invalid Timestamp Query"}, 24 | {inp: ">=" + "foobar", operator: ">=", 25 | errString: `parsing time "foobar" as "2006-01-02T15:04:05Z07:00": cannot parse "foobar" as "2006"`}, 26 | } 27 | 28 | var ct *ComparativeTime 29 | var err error 30 | for _, test := range testio { 31 | ct = NewComparativeTime() 32 | err = ct.Parse(test.inp) 33 | 34 | if ct.Operator != test.operator { 35 | t.Errorf("Expected operator %q, got %q", test.operator, ct.Operator) 36 | } 37 | 38 | if err == nil && len(test.errString) != 0 { 39 | t.Errorf("Expected error %q, got nil", test.errString) 40 | } 41 | 42 | if err != nil && err.Error() != test.errString { 43 | t.Errorf("Expected error %q, got %q", test.errString, err.Error()) 44 | } 45 | } 46 | } 47 | 48 | func TestComparativeTimeUnmarshal(t *testing.T) { 49 | type Query struct { 50 | Created ComparativeTime 51 | Modified ComparativeTime 52 | } 53 | 54 | createdTS := ">2006-01-02T15:04:05Z" 55 | updatedTS := "<=2016-01-02T15:04:05-07:00" 56 | 57 | query := url.Values{ 58 | "created": []string{createdTS}, 59 | "modified": []string{updatedTS}, 60 | } 61 | 62 | params := &Query{} 63 | err := Unmarshal(query, params) 64 | if err != nil { 65 | t.Fatal(err.Error()) 66 | } 67 | 68 | created := params.Created.String() 69 | if created != createdTS { 70 | t.Errorf("Expected created ts of %s, got %s instead.", createdTS, created) 71 | } 72 | 73 | modified := params.Modified.String() 74 | if modified != updatedTS { 75 | t.Errorf("Expected update ts of %s, got %s instead.", updatedTS, modified) 76 | } 77 | } 78 | 79 | func TestComparativeTimeMarshalString(t *testing.T) { 80 | type Query struct { 81 | Created ComparativeTime 82 | Modified ComparativeTime 83 | } 84 | 85 | createdTS := ">2006-01-02T15:04:05Z" 86 | created := NewComparativeTime() 87 | created.Parse(createdTS) 88 | updatedTS := "<=2016-01-02T15:04:05-07:00" 89 | updated := NewComparativeTime() 90 | updated.Parse(updatedTS) 91 | 92 | q := &Query{*created, *updated} 93 | result, err := MarshalString(q) 94 | if err != nil { 95 | t.Fatalf("Unable to marshal comparative timestamp: %s", err.Error()) 96 | } 97 | 98 | var unescaped string 99 | unescaped, err = url.QueryUnescape(result) 100 | if err != nil { 101 | t.Fatalf("Unable to unescape query string %q: %q", result, err.Error()) 102 | } 103 | expected := []string{"created=>2006-01-02T15:04:05Z", 104 | "modified=<=2016-01-02T15:04:05-07:00"} 105 | for _, ts := range expected { 106 | if !strings.Contains(unescaped, ts) { 107 | t.Errorf("Expected query string %s to contain %s", unescaped, ts) 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /qstring.go: -------------------------------------------------------------------------------- 1 | package qstring 2 | 3 | const ( 4 | // Tag indicates the name of the struct tag to extract from provided structs 5 | // when marshalling or unmarshalling 6 | Tag = "qstring" 7 | ) 8 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package qstring 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | // isEmptyValue returns true if the provided reflect.Value 10 | func isEmptyValue(v reflect.Value) bool { 11 | switch v.Kind() { 12 | case reflect.Array, reflect.Map, reflect.Slice, reflect.String: 13 | return v.Len() == 0 14 | case reflect.Bool: 15 | return !v.Bool() 16 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 17 | return v.Int() == 0 18 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 19 | return v.Uint() == 0 20 | case reflect.Float32, reflect.Float64: 21 | return v.Float() == 0 22 | case reflect.Interface, reflect.Ptr: 23 | return v.IsNil() 24 | case reflect.Struct: 25 | switch t := v.Interface().(type) { 26 | case time.Time: 27 | return t.IsZero() 28 | case ComparativeTime: 29 | return t.Time.IsZero() 30 | } 31 | } 32 | return false 33 | } 34 | 35 | // parseTag splits a struct field's qstring tag into its name and, if an 36 | // optional omitempty option was provided, a boolean indicating this is 37 | // returned 38 | func parseTag(tag string) (string, bool) { 39 | if idx := strings.Index(tag, ","); idx != -1 { 40 | if tag[idx+1:] == "omitempty" { 41 | return tag[:idx], true 42 | } 43 | return tag[:idx], false 44 | } 45 | return tag, false 46 | } 47 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | package qstring 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestIsEmptyValue(t *testing.T) { 10 | var ts *TestStruct 11 | ts = nil 12 | testIO := []struct { 13 | inp reflect.Value 14 | expected bool 15 | }{ 16 | {inp: reflect.ValueOf([]int{0}), expected: false}, 17 | {inp: reflect.ValueOf([]int{}), expected: true}, 18 | {inp: reflect.ValueOf(map[string]int{"a": 0}), expected: false}, 19 | {inp: reflect.ValueOf(map[string]int{}), expected: true}, 20 | {inp: reflect.ValueOf(false), expected: true}, 21 | {inp: reflect.ValueOf(true), expected: false}, 22 | {inp: reflect.ValueOf(5), expected: false}, 23 | {inp: reflect.ValueOf(0), expected: true}, 24 | {inp: reflect.ValueOf(uint(5)), expected: false}, 25 | {inp: reflect.ValueOf(uint(0)), expected: true}, 26 | {inp: reflect.ValueOf(float32(5)), expected: false}, 27 | {inp: reflect.ValueOf(float32(0)), expected: true}, 28 | {inp: reflect.ValueOf(&TestStruct{}), expected: false}, 29 | {inp: reflect.ValueOf(ts), expected: true}, 30 | {inp: reflect.ValueOf(nil), expected: false}, 31 | {inp: reflect.ValueOf(time.Time{}), expected: true}, 32 | {inp: reflect.ValueOf(time.Now()), expected: false}, 33 | {inp: reflect.ValueOf(*NewComparativeTime()), expected: true}, 34 | {inp: reflect.ValueOf(ComparativeTime{Operator: "=", Time: time.Now()}), 35 | expected: false}, 36 | } 37 | 38 | var result bool 39 | for _, test := range testIO { 40 | result = isEmptyValue(test.inp) 41 | if result != test.expected { 42 | t.Errorf("Expected %t for input %s, got %t", test.expected, test.inp, result) 43 | } 44 | } 45 | } 46 | 47 | func TestTagParsing(t *testing.T) { 48 | testio := []struct { 49 | inp string 50 | output string 51 | omit bool 52 | }{ 53 | {inp: "name,omitempty", output: "name", omit: true}, 54 | {inp: "name", output: "name", omit: false}, 55 | {inp: "name,", output: "name", omit: false}, 56 | {inp: "name", output: "name", omit: false}, 57 | {inp: "", output: "", omit: false}, 58 | {inp: ",omitempty", output: "", omit: true}, 59 | {inp: "-", output: "-", omit: false}, 60 | } 61 | 62 | var name string 63 | var omit bool 64 | for _, test := range testio { 65 | name, omit = parseTag(test.inp) 66 | if name != test.output { 67 | t.Errorf("Expected tag name to be %q, got %q instead", test.output, name) 68 | } 69 | 70 | if omit != test.omit { 71 | t.Errorf("Expected omitempty to be %t, got %t instead", test.omit, omit) 72 | } 73 | } 74 | } 75 | 76 | func TestOmitEmpty(t *testing.T) { 77 | vis := 5 78 | testio := []struct { 79 | Visible int 80 | Conditional int `qstring:"conditional,omitempty"` 81 | omit bool 82 | }{ 83 | {Visible: vis, Conditional: 5, omit: false}, 84 | {Visible: vis, Conditional: 0, omit: true}, 85 | } 86 | 87 | for _, test := range testio { 88 | values, _ := Marshal(&test) 89 | 90 | _, found := values["conditional"] 91 | if found && test.omit { 92 | t.Errorf("%d was unexpectedly marshaled", test.Conditional) 93 | } else if !found && !test.omit { 94 | t.Errorf("%d was not marshaled, but should have been", test.Conditional) 95 | } 96 | } 97 | } 98 | --------------------------------------------------------------------------------