diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..c7a4fcf
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2016 Dynamic Network Services Inc.
+
+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.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..e69de29
diff --git a/README.md b/README.md
index 0985e90..ff406a6 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,95 @@
# qstring
-Golang HTTP Query String Marshaler/Unmarshaler
+The package provides an easy way to marshal and unmarshal query string data to
+and from structs.
+
+# Installation
+```bash
+$ go get github.com/dyninc/qstring
+```
+
+# Example
+
+## Literal
+
+```go
+package main
+
+import (
+ "net/http"
+
+ "github.com/dyninc/qstring"
+)
+
+// Query is the request query struct.
+type Query struct {
+ Names []string
+ Limit int
+ Page int
+}
+
+func handler(w http.ResponseWriter, req *http.Request) {
+ query := &Query{}
+ err := qstring.Unmarshal(req.Url.Query(), query)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ }
+
+ // ... run conditional logic based on provided query parameters
+}
+```
+
+## Nested
+In the same spirit as other Unmarshalling libraries, `qstring` allows you to
+Marshal/Unmarshal nested structs
+
+```go
+package main
+
+import (
+ "net/http"
+
+ "github.com/dyninc/qstring"
+)
+
+// PagingParams represents common pagination information for query strings
+type PagingParams struct {
+ Page int
+ Limit int
+}
+
+// Req is the request query struct.
+type Query struct {
+ Names []string
+ PageInfo PagingParams
+}
+```
+
+## Complex Structures
+Again, in the spirit of other Unmarshalling libraries, `qstring` allows for some
+more complex types, such as pointers and time.Time fields. *Note: All Timestamps
+are assumed to be in RFC3339 format*.
+
+```go
+package main
+
+import (
+ "net/http"
+ "time"
+
+ "github.com/dyninc/qstring"
+)
+
+// PagingParams represents common pagination information for query strings
+type PagingParams struct {
+ Page int
+ Limit int
+}
+
+// Req is the request query struct.
+type Query struct {
+ Names []string
+ PageInfo *PagingParams
+ Created time.TIme
+ Modified time.Time
+}
+```
diff --git a/bench_test.go b/bench_test.go
new file mode 100644
index 0000000..b2c9cf3
--- /dev/null
+++ b/bench_test.go
@@ -0,0 +1,52 @@
+package qstring
+
+import (
+ "net/url"
+ "testing"
+)
+
+// Straight benchmark literal.
+func BenchmarkUnmarshall(b *testing.B) {
+ query := url.Values{
+ "limit": []string{"10"},
+ "page": []string{"1"},
+ "fields": []string{"a", "b", "c"},
+ }
+ type QueryStruct struct {
+ Fields []string
+ Limit int
+ Page int
+ }
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ data := &QueryStruct{}
+ err := Unmarshal(query, data)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+// Parallel benchmark literal.
+func BenchmarkRawPLiteral(b *testing.B) {
+ query := url.Values{
+ "limit": []string{"10"},
+ "page": []string{"1"},
+ "fields": []string{"a", "b", "c"},
+ }
+ type QueryStruct struct {
+ Fields []string
+ Limit int
+ Page int
+ }
+ b.ResetTimer()
+ b.RunParallel(func(pb *testing.PB) {
+ for pb.Next() {
+ data := &QueryStruct{}
+ err := Unmarshal(query, data)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+}
diff --git a/coverage.html b/coverage.html
new file mode 100644
index 0000000..36fe38b
--- /dev/null
+++ b/coverage.html
@@ -0,0 +1,443 @@
+
+
+
+
+
+
+
package qstring
+
+import (
+ "net/url"
+ "reflect"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// Unmarshaller defines the interface for performing custom unmarshalling of
+// query strings into struct values
+type Unmarshaller interface {
+ UnmarshalQuery(url.Values) error
+}
+
+// Unmarshal unmarshalls the provided url.Values (query string) into the
+// interface provided
+func Unmarshal(data url.Values, v interface{}) error {
+ var d decoder
+ d.init(data)
+ return d.unmarshal(v)
+}
+
+// An InvalidUnmarshalError describes an invalid argument passed to Unmarshal.
+// (The argument to Unmarshal must be a non-nil pointer.)
+type InvalidUnmarshalError struct {
+ Type reflect.Type
+}
+
+func (e InvalidUnmarshalError) Error() string {
+ if e.Type == nil {
+ return "qstring: Unmarshal(nil)"
+ }
+
+ if e.Type.Kind() != reflect.Ptr {
+ return "qstring: Unmarshal(non-pointer " + e.Type.String() + ")"
+ }
+ return "qstring: Unmarshal(nil " + e.Type.String() + ")"
+}
+
+type decoder struct {
+ data url.Values
+}
+
+func (d *decoder) init(data url.Values) *decoder {
+ d.data = data
+ return d
+}
+
+func (d *decoder) unmarshal(v interface{}) error {
+ rv := reflect.ValueOf(v)
+ if rv.Kind() != reflect.Ptr || rv.IsNil() {
+ return &InvalidUnmarshalError{reflect.TypeOf(v)}
+ }
+
+ return d.value(rv)
+}
+
+func (d *decoder) value(val reflect.Value) error {
+ elem := val.Elem()
+ typ := elem.Type()
+ for i := 0; i < elem.NumField(); i++ {
+ // pull out the qstring struct tag
+ elemField := elem.Field(i)
+ typField := typ.Field(i)
+ qstring := typField.Tag.Get(Tag)
+ if qstring == "" {
+ // resolvable fields must have at least the `flag` struct tag
+ qstring = strings.ToLower(typField.Name)
+ }
+
+ // determine if this is an unsettable field or was explicitly set to be
+ // ignored
+ if !elemField.CanSet() || qstring == "-" {
+ continue
+ }
+
+ // only do work if the current fields query string parameter was provided
+ if query, ok := d.data[qstring]; ok {
+ switch k := typField.Type.Kind(); k {
+ case reflect.Slice:
+ d.coerceSlice(query, k, elemField)
+ default:
+ d.coerce(query[0], k, elemField)
+ }
+ } else if typField.Type.Kind() == reflect.Struct {
+ if elemField.CanAddr() {
+ d.value(elemField.Addr())
+ }
+ }
+ }
+ return nil
+}
+
+// coerce converts the provided query parameter slice into the proper type for
+// the target field. this coerced value is then assigned to the current field
+func (d *decoder) coerce(query string, target reflect.Kind, field reflect.Value) error {
+ var err error
+ var c interface{}
+
+ switch target {
+ case reflect.String:
+ field.SetString(query)
+ case reflect.Bool:
+ c, err = strconv.ParseBool(query)
+ if err == nil {
+ field.SetBool(c.(bool))
+ }
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ c, err = strconv.ParseInt(query, 10, 64)
+ if err == nil {
+ field.SetInt(c.(int64))
+ }
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ c, err = strconv.ParseUint(query, 10, 64)
+ if err == nil {
+ field.SetUint(c.(uint64))
+ }
+ case reflect.Float32, reflect.Float64:
+ c, err = strconv.ParseFloat(query, 64)
+ if err == nil {
+ field.SetFloat(c.(float64))
+ }
+ case reflect.Struct:
+ switch field.Interface().(type) {
+ case time.Time:
+ var t time.Time
+ t, err = time.Parse(time.RFC3339, query)
+ if err == nil {
+ field.Set(reflect.ValueOf(t))
+ }
+ default:
+ d.value(field)
+ }
+ }
+
+ return err
+}
+
+// coerceSlice creates a new slice of the appropriate type for the target field
+// and coerces each of the query parameter values into the destination type.
+// Should any of the provided query parameters fail to be coerced, an error is
+// returned and the entire slice will not be applied
+func (d *decoder) coerceSlice(query []string, target reflect.Kind, field reflect.Value) error {
+ var err error
+ sliceType := field.Type().Elem()
+ coerceKind := sliceType.Kind()
+ sl := reflect.MakeSlice(reflect.SliceOf(sliceType), 0, 0)
+ // Create a pointer to a slice value and set it to the slice
+ slice := reflect.New(sl.Type())
+ slice.Elem().Set(sl)
+ for _, q := range query {
+ val := reflect.New(sliceType).Elem()
+ err = d.coerce(q, coerceKind, val)
+ if err != nil {
+ return err
+ }
+ slice.Elem().Set(reflect.Append(slice.Elem(), val))
+ }
+ field.Set(slice.Elem())
+ return nil
+}
+
+
+
package qstring
+
+import (
+ "net/url"
+ "reflect"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// Marshaller defines the interface for performing custom marshalling of struct
+// values into query strings
+type Marshaller interface {
+ MarshalQuery() (url.Values, error)
+}
+
+// MarshalValues marshals the provided struct into a url.Values collection
+func MarshalValues(v interface{}) (url.Values, error) {
+ var e encoder
+ e.init(v)
+ return e.marshal()
+}
+
+// Marshal marshals the provided struct into a raw query string and returns a
+// conditional error
+func Marshal(v interface{}) (string, error) {
+ vals, err := MarshalValues(v)
+ if err != nil {
+ return "", err
+ }
+ return vals.Encode(), nil
+}
+
+// An InvalidMarshalError describes an invalid argument passed to Marshal or
+// MarshalValue. (The argument to Marshal must be a non-nil pointer.)
+type InvalidMarshalError struct {
+ Type reflect.Type
+}
+
+func (e InvalidMarshalError) Error() string {
+ if e.Type == nil {
+ return "qstring: Marshal(nil)"
+ }
+
+ if e.Type.Kind() != reflect.Ptr {
+ return "qstring: Marshal(non-pointer " + e.Type.String() + ")"
+ }
+ return "qstring: Marshal(nil " + e.Type.String() + ")"
+}
+
+type encoder struct {
+ data interface{}
+}
+
+func (e *encoder) init(v interface{}) *encoder {
+ e.data = v
+ return e
+}
+
+func (e *encoder) marshal() (url.Values, error) {
+ rv := reflect.ValueOf(e.data)
+ if rv.Kind() != reflect.Ptr || rv.IsNil() {
+ return nil, &InvalidMarshalError{reflect.TypeOf(e.data)}
+ }
+
+ return e.value(rv)
+}
+
+func (e *encoder) value(val reflect.Value) (url.Values, error) {
+ elem := val.Elem()
+ typ := elem.Type()
+
+ var err error
+ var output = make(url.Values)
+ for i := 0; i < elem.NumField(); i++ {
+ // pull out the qstring struct tag
+ elemField := elem.Field(i)
+ typField := typ.Field(i)
+ qstring := typField.Tag.Get(Tag)
+ if qstring == "" {
+ // resolvable fields must have at least the `flag` struct tag
+ qstring = strings.ToLower(typField.Name)
+ }
+
+ // determine if this is an unsettable field or was explicitly set to be
+ // ignored
+ if !elemField.CanSet() || qstring == "-" || isEmptyValue(elemField) {
+ continue
+ }
+
+ // only do work if the current fields query string parameter was provided
+ switch k := typField.Type.Kind(); k {
+ case reflect.Slice:
+ output[qstring] = marshalSlice(elemField)
+ case reflect.Struct:
+ switch elemField.Interface().(type) {
+ case time.Time:
+ output.Set(qstring, marshalValue(elemField, k))
+ default:
+ var vals url.Values
+ if elemField.CanAddr() {
+ vals, err = MarshalValues(elemField.Addr().Interface())
+ }
+
+ if err != nil {
+ return output, err
+ }
+ for key, list := range vals {
+ output[key] = list
+ }
+ }
+ default:
+ output.Set(qstring, marshalValue(elemField, k))
+ }
+ }
+ return output, err
+}
+
+func marshalSlice(field reflect.Value) []string {
+ var out []string
+ for i := 0; i < field.Len(); i++ {
+ out = append(out, marshalValue(field.Index(i), field.Index(i).Kind()))
+ }
+ return out
+}
+
+func marshalValue(field reflect.Value, source reflect.Kind) string {
+ switch source {
+ case reflect.String:
+ return field.String()
+ case reflect.Bool:
+ return strconv.FormatBool(field.Bool())
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ return strconv.FormatInt(field.Int(), 10)
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ return strconv.FormatUint(field.Uint(), 10)
+ case reflect.Float32, reflect.Float64:
+ return strconv.FormatFloat(field.Float(), 'G', -1, 64)
+ case reflect.Struct:
+ switch field.Interface().(type) {
+ case time.Time:
+ return field.Interface().(time.Time).Format(time.RFC3339)
+ }
+ }
+ return ""
+}
+
+
+
package qstring
+
+import (
+ "reflect"
+)
+
+// isEmptyValue returns true if the provided reflect.Value
+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()
+ }
+ return false
+}
+
+
+
+
+
+
diff --git a/coverage.out b/coverage.out
new file mode 100644
index 0000000..6e79be0
--- /dev/null
+++ b/coverage.out
@@ -0,0 +1,97 @@
+mode: set
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:19.54,23.2 3 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:31.47,32.19 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:36.2,36.34 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:39.2,39.58 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:32.19,34.3 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:36.34,38.3 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:46.50,49.2 2 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:51.50,53.44 2 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:57.2,57.20 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:53.44,55.3 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:60.50,63.39 3 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:93.2,93.12 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:63.39,68.20 4 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:75.3,75.44 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:80.3,80.39 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:68.20,71.4 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:75.44,76.12 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:80.39,81.40 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:82.4,83.39 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:84.4,85.37 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:87.4,87.52 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:87.52,88.27 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:88.27,90.5 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:98.88,102.16 3 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:138.2,138.12 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:103.2,104.25 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:105.2,107.17 2 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:110.2,112.17 2 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:115.2,117.17 2 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:120.2,122.17 2 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:125.2,126.35 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:107.17,109.4 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:112.17,114.4 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:117.17,119.4 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:122.17,124.4 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:127.3,130.18 3 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:133.3,134.18 1 0
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:130.18,132.5 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:145.95,153.26 7 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:161.2,162.12 2 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:153.26,156.17 3 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:159.3,159.54 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/decode.go:156.17,158.4 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:18.55,22.2 3 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:26.45,28.16 2 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:31.2,31.27 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:28.16,30.3 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:40.45,41.19 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:45.2,45.34 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:48.2,48.56 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:41.19,43.3 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:45.34,47.3 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:55.48,58.2 2 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:60.49,62.44 2 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:66.2,66.20 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:62.44,64.3 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:69.64,75.39 5 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:103.2,103.20 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:75.39,80.20 4 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:87.3,87.71 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:92.3,92.39 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:80.20,83.4 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:87.71,88.12 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:93.3,94.51 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:95.3,96.45 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:97.3,98.66 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:99.3,100.48 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:106.49,108.35 2 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:111.2,111.12 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:108.35,110.3 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:114.68,115.16 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:132.2,132.11 1 0
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:116.2,117.24 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:118.2,119.42 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:120.2,121.44 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:122.2,123.46 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:124.2,125.57 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:126.2,127.35 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:128.3,129.61 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:135.103,137.34 2 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:153.2,153.12 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:138.2,139.51 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:140.2,142.22 2 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:146.3,146.17 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:149.3,149.31 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:142.22,144.4 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:146.17,148.4 1 0
+github.corp.dyndns.com/Zephyr/coredns/qstring/encode.go:149.31,151.4 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/utils.go:8.41,9.18 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/utils.go:23.2,23.14 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/utils.go:10.2,11.22 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/utils.go:12.2,13.19 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/utils.go:14.2,15.22 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/utils.go:16.2,17.23 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/utils.go:18.2,19.24 1 1
+github.corp.dyndns.com/Zephyr/coredns/qstring/utils.go:20.2,21.19 1 1
diff --git a/decode.go b/decode.go
new file mode 100644
index 0000000..852dde3
--- /dev/null
+++ b/decode.go
@@ -0,0 +1,163 @@
+package qstring
+
+import (
+ "net/url"
+ "reflect"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// Unmarshaller defines the interface for performing custom unmarshalling of
+// query strings into struct values
+type Unmarshaller interface {
+ UnmarshalQuery(url.Values) error
+}
+
+// Unmarshal unmarshalls the provided url.Values (query string) into the
+// interface provided
+func Unmarshal(data url.Values, v interface{}) error {
+ var d decoder
+ d.init(data)
+ return d.unmarshal(v)
+}
+
+// An InvalidUnmarshalError describes an invalid argument passed to Unmarshal.
+// (The argument to Unmarshal must be a non-nil pointer.)
+type InvalidUnmarshalError struct {
+ Type reflect.Type
+}
+
+func (e InvalidUnmarshalError) Error() string {
+ if e.Type == nil {
+ return "qstring: Unmarshal(nil)"
+ }
+
+ if e.Type.Kind() != reflect.Ptr {
+ return "qstring: Unmarshal(non-pointer " + e.Type.String() + ")"
+ }
+ return "qstring: Unmarshal(nil " + e.Type.String() + ")"
+}
+
+type decoder struct {
+ data url.Values
+}
+
+func (d *decoder) init(data url.Values) *decoder {
+ d.data = data
+ return d
+}
+
+func (d *decoder) unmarshal(v interface{}) error {
+ rv := reflect.ValueOf(v)
+ if rv.Kind() != reflect.Ptr || rv.IsNil() {
+ return &InvalidUnmarshalError{reflect.TypeOf(v)}
+ }
+
+ return d.value(rv)
+}
+
+func (d *decoder) value(val reflect.Value) error {
+ elem := val.Elem()
+ typ := elem.Type()
+ for i := 0; i < elem.NumField(); i++ {
+ // pull out the qstring struct tag
+ elemField := elem.Field(i)
+ typField := typ.Field(i)
+ qstring := typField.Tag.Get(Tag)
+ if qstring == "" {
+ // resolvable fields must have at least the `flag` struct tag
+ qstring = strings.ToLower(typField.Name)
+ }
+
+ // determine if this is an unsettable field or was explicitly set to be
+ // ignored
+ if !elemField.CanSet() || qstring == "-" {
+ continue
+ }
+
+ // only do work if the current fields query string parameter was provided
+ if query, ok := d.data[qstring]; ok {
+ switch k := typField.Type.Kind(); k {
+ case reflect.Slice:
+ d.coerceSlice(query, k, elemField)
+ default:
+ d.coerce(query[0], k, elemField)
+ }
+ } else if typField.Type.Kind() == reflect.Struct {
+ if elemField.CanAddr() {
+ d.value(elemField.Addr())
+ }
+ }
+ }
+ return nil
+}
+
+// coerce converts the provided query parameter slice into the proper type for
+// the target field. this coerced value is then assigned to the current field
+func (d *decoder) coerce(query string, target reflect.Kind, field reflect.Value) error {
+ var err error
+ var c interface{}
+
+ switch target {
+ case reflect.String:
+ field.SetString(query)
+ case reflect.Bool:
+ c, err = strconv.ParseBool(query)
+ if err == nil {
+ field.SetBool(c.(bool))
+ }
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ c, err = strconv.ParseInt(query, 10, 64)
+ if err == nil {
+ field.SetInt(c.(int64))
+ }
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ c, err = strconv.ParseUint(query, 10, 64)
+ if err == nil {
+ field.SetUint(c.(uint64))
+ }
+ case reflect.Float32, reflect.Float64:
+ c, err = strconv.ParseFloat(query, 64)
+ if err == nil {
+ field.SetFloat(c.(float64))
+ }
+ case reflect.Struct:
+ switch field.Interface().(type) {
+ case time.Time:
+ var t time.Time
+ t, err = time.Parse(time.RFC3339, query)
+ if err == nil {
+ field.Set(reflect.ValueOf(t))
+ }
+ default:
+ d.value(field)
+ }
+ }
+
+ return err
+}
+
+// coerceSlice creates a new slice of the appropriate type for the target field
+// and coerces each of the query parameter values into the destination type.
+// Should any of the provided query parameters fail to be coerced, an error is
+// returned and the entire slice will not be applied
+func (d *decoder) coerceSlice(query []string, target reflect.Kind, field reflect.Value) error {
+ var err error
+ sliceType := field.Type().Elem()
+ coerceKind := sliceType.Kind()
+ sl := reflect.MakeSlice(reflect.SliceOf(sliceType), 0, 0)
+ // Create a pointer to a slice value and set it to the slice
+ slice := reflect.New(sl.Type())
+ slice.Elem().Set(sl)
+ for _, q := range query {
+ val := reflect.New(sliceType).Elem()
+ err = d.coerce(q, coerceKind, val)
+ if err != nil {
+ return err
+ }
+ slice.Elem().Set(reflect.Append(slice.Elem(), val))
+ }
+ field.Set(slice.Elem())
+ return nil
+}
diff --git a/decode_test.go b/decode_test.go
new file mode 100644
index 0000000..ac08252
--- /dev/null
+++ b/decode_test.go
@@ -0,0 +1,181 @@
+package qstring
+
+import (
+ "net/url"
+ "testing"
+ "time"
+)
+
+type TestStruct struct {
+ Name string `qstring:"name"`
+ Do bool
+
+ // int fields
+ Page int `qstring:"page"`
+ ID int8
+ Small int16
+ Med int32
+ Big int64
+
+ // uint fields
+ UPage uint
+ UID uint8
+ USmall uint16
+ UMed uint32
+ UBig uint64
+
+ // Floats
+ Float32 float32
+ Float64 float64
+
+ // slice fields
+ Fields []string `qstring:"fields"`
+ DoFields []bool `qstring:"dofields"`
+ Counts []int
+ IDs []int8
+ Smalls []int16
+ Meds []int32
+ Bigs []int64
+
+ // uint fields
+ UPages []uint
+ UIDs []uint8
+ USmalls []uint16
+ UMeds []uint32
+ UBigs []uint64
+
+ // Floats
+ Float32s []float32
+ Float64s []float64
+ hidden int
+ Hidden int `qstring:"-"`
+}
+
+func TestUnmarshall(t *testing.T) {
+ var ts TestStruct
+ query := url.Values{
+ "name": []string{"SomeName"},
+ "do": []string{"true"},
+ "page": []string{"1"},
+ "id": []string{"12"},
+ "small": []string{"13"},
+ "med": []string{"14"},
+ "big": []string{"15"},
+ "upage": []string{"2"},
+ "uid": []string{"16"},
+ "usmall": []string{"17"},
+ "umed": []string{"18"},
+ "ubig": []string{"19"},
+ "float32": []string{"6000"},
+ "float64": []string{"7000"},
+ "fields": []string{"foo", "bar"},
+ "dofields": []string{"true", "false"},
+ "counts": []string{"1", "2"},
+ "ids": []string{"3", "4", "5"},
+ "smalls": []string{"6", "7", "8"},
+ "meds": []string{"9", "10", "11"},
+ "bigs": []string{"12", "13", "14"},
+ "upages": []string{"2", "3", "4"},
+ "uids": []string{"5, 6, 7"},
+ "usmalls": []string{"8", "9", "10"},
+ "umeds": []string{"9", "10", "11"},
+ "ubigs": []string{"12", "13", "14"},
+ "float32s": []string{"6000", "6001", "6002"},
+ "float64s": []string{"7000", "7001", "7002"},
+ }
+
+ err := Unmarshal(query, &ts)
+ if err != nil {
+ t.Fatal(err.Error())
+ }
+
+ if ts.Page != 1 {
+ t.Errorf("Expected page to be 1, got %d", ts.Page)
+ }
+
+ if len(ts.Fields) != 2 {
+ t.Errorf("Expected 2 fields, got %d", len(ts.Fields))
+ }
+}
+
+func TestUnmarshalNested(t *testing.T) {
+ type Paging struct {
+ Page int
+ Limit int
+ }
+
+ type Params struct {
+ Paging Paging
+ Name string
+ }
+
+ query := url.Values{
+ "name": []string{"SomeName"},
+ "page": []string{"1"},
+ "limit": []string{"50"},
+ }
+
+ params := &Params{}
+
+ err := Unmarshal(query, params)
+ if err != nil {
+ t.Fatal(err.Error())
+ }
+
+ if params.Paging.Page != 1 {
+ t.Errorf("Nested Struct Failed to Unmarshal. Expected 1, got %d", params.Paging.Page)
+ }
+}
+
+func TestUnmarshalTime(t *testing.T) {
+ type Query struct {
+ Created time.Time
+ LastUpdated time.Time
+ }
+
+ createdTS := "2006-01-02T15:04:05Z"
+ updatedTS := "2016-01-02T15:04:05-07:00"
+
+ query := url.Values{
+ "created": []string{createdTS},
+ "lastupdated": []string{updatedTS},
+ }
+
+ params := &Query{}
+ err := Unmarshal(query, params)
+ if err != nil {
+ t.Fatal(err.Error())
+ }
+
+ if params.Created.Format(time.RFC3339) != createdTS {
+ t.Errorf("Expected created ts of %s, got %s instead.", createdTS, params.Created.Format(time.RFC3339))
+ }
+
+ if params.LastUpdated.Format(time.RFC3339) != updatedTS {
+ t.Errorf("Expected update ts of %s, got %s instead.", updatedTS, params.LastUpdated.Format(time.RFC3339))
+ }
+}
+
+func TestUnmarshalInvalidTypes(t *testing.T) {
+ var err error
+ var ts *TestStruct
+ testio := []struct {
+ inp interface{}
+ errString string
+ }{
+ {inp: nil, errString: "qstring: Unmarshal(nil)"},
+ {inp: TestStruct{}, errString: "qstring: Unmarshal(non-pointer qstring.TestStruct)"},
+ {inp: ts, errString: "qstring: Unmarshal(nil *qstring.TestStruct)"},
+ }
+
+ for _, test := range testio {
+ err = Unmarshal(url.Values{}, test.inp)
+ if err == nil {
+ t.Errorf("Expected invalid type error, got success instead")
+ }
+
+ if err.Error() != test.errString {
+ t.Errorf("Got %q error, expected %q", err.Error(), test.errString)
+ }
+ }
+}
diff --git a/encode.go b/encode.go
new file mode 100644
index 0000000..0b586f9
--- /dev/null
+++ b/encode.go
@@ -0,0 +1,154 @@
+package qstring
+
+import (
+ "net/url"
+ "reflect"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// Marshaller defines the interface for performing custom marshalling of struct
+// values into query strings
+type Marshaller interface {
+ MarshalQuery() (url.Values, error)
+}
+
+// MarshalValues marshals the provided struct into a url.Values collection
+func MarshalValues(v interface{}) (url.Values, error) {
+ var e encoder
+ e.init(v)
+ return e.marshal()
+}
+
+// Marshal marshals the provided struct into a raw query string and returns a
+// conditional error
+func Marshal(v interface{}) (string, error) {
+ vals, err := MarshalValues(v)
+ if err != nil {
+ return "", err
+ }
+ return vals.Encode(), nil
+}
+
+// An InvalidMarshalError describes an invalid argument passed to Marshal or
+// MarshalValue. (The argument to Marshal must be a non-nil pointer.)
+type InvalidMarshalError struct {
+ Type reflect.Type
+}
+
+func (e InvalidMarshalError) Error() string {
+ if e.Type == nil {
+ return "qstring: Marshal(nil)"
+ }
+
+ if e.Type.Kind() != reflect.Ptr {
+ return "qstring: Marshal(non-pointer " + e.Type.String() + ")"
+ }
+ return "qstring: Marshal(nil " + e.Type.String() + ")"
+}
+
+type encoder struct {
+ data interface{}
+}
+
+func (e *encoder) init(v interface{}) *encoder {
+ e.data = v
+ return e
+}
+
+func (e *encoder) marshal() (url.Values, error) {
+ rv := reflect.ValueOf(e.data)
+ if rv.Kind() != reflect.Ptr || rv.IsNil() {
+ return nil, &InvalidMarshalError{reflect.TypeOf(e.data)}
+ }
+
+ return e.value(rv)
+}
+
+func (e *encoder) value(val reflect.Value) (url.Values, error) {
+ elem := val.Elem()
+ typ := elem.Type()
+
+ var err error
+ var output = make(url.Values)
+ for i := 0; i < elem.NumField(); i++ {
+ // pull out the qstring struct tag
+ elemField := elem.Field(i)
+ typField := typ.Field(i)
+ qstring := typField.Tag.Get(Tag)
+ if qstring == "" {
+ // resolvable fields must have at least the `flag` struct tag
+ qstring = strings.ToLower(typField.Name)
+ }
+
+ // determine if this is an unsettable field or was explicitly set to be
+ // ignored
+ if !elemField.CanSet() || qstring == "-" || isEmptyValue(elemField) {
+ continue
+ }
+
+ // only do work if the current fields query string parameter was provided
+ switch k := typField.Type.Kind(); k {
+ default:
+ output.Set(qstring, marshalValue(elemField, k))
+ case reflect.Slice:
+ output[qstring] = marshalSlice(elemField)
+ case reflect.Ptr:
+ marshalStruct(output, qstring, reflect.Indirect(elemField), k)
+ case reflect.Struct:
+ marshalStruct(output, qstring, elemField, k)
+ }
+ }
+ return output, err
+}
+
+func marshalSlice(field reflect.Value) []string {
+ var out []string
+ for i := 0; i < field.Len(); i++ {
+ out = append(out, marshalValue(field.Index(i), field.Index(i).Kind()))
+ }
+ return out
+}
+
+func marshalValue(field reflect.Value, source reflect.Kind) string {
+ switch source {
+ case reflect.String:
+ return field.String()
+ case reflect.Bool:
+ return strconv.FormatBool(field.Bool())
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ return strconv.FormatInt(field.Int(), 10)
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ return strconv.FormatUint(field.Uint(), 10)
+ case reflect.Float32, reflect.Float64:
+ return strconv.FormatFloat(field.Float(), 'G', -1, 64)
+ case reflect.Struct:
+ switch field.Interface().(type) {
+ case time.Time:
+ return field.Interface().(time.Time).Format(time.RFC3339)
+ }
+ }
+ return ""
+}
+
+func marshalStruct(output url.Values, qstring string, field reflect.Value, source reflect.Kind) error {
+ var err error
+ switch field.Interface().(type) {
+ case time.Time:
+ output.Set(qstring, marshalValue(field, source))
+ default:
+ var vals url.Values
+ if field.CanAddr() {
+ vals, err = MarshalValues(field.Addr().Interface())
+ }
+
+ if err != nil {
+ return err
+ }
+ for key, list := range vals {
+ output[key] = list
+ }
+ }
+ return nil
+}
diff --git a/encode_test.go b/encode_test.go
new file mode 100644
index 0000000..dd3990a
--- /dev/null
+++ b/encode_test.go
@@ -0,0 +1,268 @@
+package qstring
+
+import (
+ "net/url"
+ "strings"
+ "testing"
+ "time"
+)
+
+func TestMarshallString(t *testing.T) {
+ ts := TestStruct{
+ Name: "SomeName",
+ Do: true,
+ Page: 1,
+ ID: 12,
+ Small: 13,
+ Med: 14,
+ Big: 15,
+ UPage: 2,
+ UID: 16,
+ USmall: 17,
+ UMed: 17,
+ UBig: 17,
+ Float32: 6000,
+ Float64: 7000,
+ Fields: []string{"foo", "bar"},
+ DoFields: []bool{true, false},
+ Counts: []int{1, 2},
+ IDs: []int8{3, 4},
+ Smalls: []int16{6, 7},
+ Meds: []int32{9, 10},
+ Bigs: []int64{12, 13},
+ UPages: []uint{2, 3},
+ UIDs: []uint8{5, 6},
+ USmalls: []uint16{8, 9},
+ UMeds: []uint32{9, 10},
+ UBigs: []uint64{12, 13},
+ Float32s: []float32{6000, 6001},
+ Float64s: []float64{7000, 7001},
+ }
+
+ expected := []string{"name=SomeName", "do=true", "page=1", "id=12", "small=13",
+ "med=14", "big=15", "upage=2", "uid=16", "usmall=17", "umed=17", "ubig=17",
+ "float32=6000", "float64=7000", "fields=foo", "fields=bar", "dofields=true",
+ "dofields=false", "counts=1", "counts=2", "ids=3", "ids=4", "smalls=6",
+ "smalls=7", "meds=9", "meds=10", "bigs=12", "bigs=13", "upages=2",
+ "upages=3", "uids=5", "uids=6", "usmalls=8", "usmalls=9", "umeds=9",
+ "umeds=10", "ubigs=12", "ubigs=13", "float32s=6000", "float32s=6001",
+ "float64s=7000", "float64s=7001"}
+ query, err := Marshal(&ts)
+ if err != nil {
+ t.Fatal(err.Error())
+ }
+
+ for _, param := range expected {
+ if !strings.Contains(query, param) {
+ t.Errorf("Expected %s to contain %s", query, param)
+ }
+ }
+}
+
+func TestMarshallValues(t *testing.T) {
+ ts := TestStruct{
+ Name: "SomeName",
+ Do: true,
+ Page: 1,
+ ID: 12,
+ Small: 13,
+ Med: 14,
+ Big: 15,
+ UPage: 2,
+ UID: 16,
+ USmall: 17,
+ UMed: 17,
+ UBig: 17,
+ Float32: 6000,
+ Float64: 7000,
+ Fields: []string{"foo", "bar"},
+ DoFields: []bool{true, false},
+ Counts: []int{1, 2},
+ IDs: []int8{3, 4},
+ Smalls: []int16{6, 7},
+ Meds: []int32{9, 10},
+ Bigs: []int64{12, 13},
+ UPages: []uint{2, 3},
+ UIDs: []uint8{5, 6},
+ USmalls: []uint16{8, 9},
+ UMeds: []uint32{9, 10},
+ UBigs: []uint64{12, 13},
+ Float32s: []float32{6000, 6001},
+ Float64s: []float64{7000, 7001},
+ }
+
+ expected := url.Values{
+ "name": []string{"SomeName"},
+ "do": []string{"true"},
+ "page": []string{"1"},
+ "id": []string{"12"},
+ "small": []string{"13"},
+ "med": []string{"14"},
+ "big": []string{"15"},
+ "upage": []string{"2"},
+ "uid": []string{"16"},
+ "usmall": []string{"17"},
+ "umed": []string{"18"},
+ "ubig": []string{"19"},
+ "float32": []string{"6000"},
+ "float64": []string{"7000"},
+ "fields": []string{"foo", "bar"},
+ "dofields": []string{"true", "false"},
+ "counts": []string{"1", "2"},
+ "ids": []string{"3", "4", "5"},
+ "smalls": []string{"6", "7", "8"},
+ "meds": []string{"9", "10", "11"},
+ "bigs": []string{"12", "13", "14"},
+ "upages": []string{"2", "3", "4"},
+ "uids": []string{"5, 6, 7"},
+ "usmalls": []string{"8", "9", "10"},
+ "umeds": []string{"9", "10", "11"},
+ "ubigs": []string{"12", "13", "14"},
+ "float32s": []string{"6000", "6001", "6002"},
+ "float64s": []string{"7000", "7001", "7002"},
+ }
+ values, err := MarshalValues(&ts)
+ if err != nil {
+ t.Fatal(err.Error())
+ }
+
+ if len(values) != len(expected) {
+ t.Errorf("Expected %d fields, got %d", len(expected), len(values))
+ }
+}
+
+func TestInvalidMarshal(t *testing.T) {
+ var err error
+ var ts *TestStruct
+ testio := []struct {
+ inp interface{}
+ errString string
+ }{
+ {inp: nil, errString: "qstring: Marshal(nil)"},
+ {inp: TestStruct{}, errString: "qstring: Marshal(non-pointer qstring.TestStruct)"},
+ {inp: ts, errString: "qstring: Marshal(nil *qstring.TestStruct)"},
+ }
+
+ for _, test := range testio {
+ _, err = Marshal(test.inp)
+ if err == nil {
+ t.Errorf("Expected invalid type error, got success instead")
+ }
+
+ if err.Error() != test.errString {
+ t.Errorf("Got %q error, expected %q", err.Error(), test.errString)
+ }
+ }
+}
+
+func TestMarshalTime(t *testing.T) {
+ type Query struct {
+ Created time.Time
+ LastUpdated time.Time
+ }
+
+ createdTS := "2006-01-02T15:04:05Z"
+ createdTime, _ := time.Parse(time.RFC3339, createdTS)
+ updatedTS := "2016-01-02T15:04:05-07:00"
+ updatedTime, _ := time.Parse(time.RFC3339, updatedTS)
+
+ q := &Query{Created: createdTime, LastUpdated: updatedTime}
+ result, err := Marshal(q)
+ if err != nil {
+ t.Fatalf("Unable to marshal timestamp: %s", err.Error())
+ }
+
+ var unescaped string
+ unescaped, err = url.QueryUnescape(result)
+ if err != nil {
+ t.Fatalf("Unable to unescape query string %q: %q", result, err.Error())
+ }
+
+ expected := []string{"created=2006-01-02T15:04:05Z",
+ "lastupdated=2016-01-02T15:04:05-07:00"}
+ for _, ts := range expected {
+ if !strings.Contains(unescaped, ts) {
+ t.Errorf("Expected query string %s to contain %s", unescaped, ts)
+ }
+ }
+}
+
+func TestMarshalNested(t *testing.T) {
+ type Paging struct {
+ Page int
+ Limit int
+ }
+
+ type Params struct {
+ Paging Paging
+ Name string
+ }
+
+ params := &Params{Name: "SomeName",
+ Paging: Paging{Page: 1, Limit: 50},
+ }
+
+ result, err := Marshal(params)
+ if err != nil {
+ t.Fatalf("Unable to marshal nested struct: %s", err.Error())
+ }
+
+ var unescaped string
+ unescaped, err = url.QueryUnescape(result)
+ if err != nil {
+ t.Fatalf("Unable to unescape query string %q: %q", result, err.Error())
+ }
+
+ // ensure the nested struct isn't iteself included in the query string
+ if strings.Contains(unescaped, "paging=") {
+ t.Errorf("Nested struct was included in %q", unescaped)
+ }
+
+ // ensure fields we expect to be present are
+ expected := []string{"name=SomeName", "page=1", "limit=50"}
+ for _, q := range expected {
+ if !strings.Contains(unescaped, q) {
+ t.Errorf("Expected query string %s to contain %s", unescaped, q)
+ }
+ }
+}
+
+func TestMarshalNestedPtrs(t *testing.T) {
+ type Paging struct {
+ Page int
+ Limit int
+ }
+
+ type Params struct {
+ Paging *Paging
+ Name string
+ }
+
+ params := &Params{Name: "SomeName",
+ Paging: &Paging{Page: 1, Limit: 50},
+ }
+
+ result, err := Marshal(params)
+ if err != nil {
+ t.Fatalf("Unable to marshal nested struct: %s", err.Error())
+ }
+
+ var unescaped string
+ unescaped, err = url.QueryUnescape(result)
+ if err != nil {
+ t.Fatalf("Unable to unescape query string %q: %q", result, err.Error())
+ }
+
+ // ensure the nested struct isn't iteself included in the query string
+ if strings.Contains(unescaped, "paging=") {
+ t.Errorf("Nested struct was included in %q", unescaped)
+ }
+
+ // ensure fields we expect to be present are
+ expected := []string{"name=SomeName", "page=1", "limit=50"}
+ for _, q := range expected {
+ if !strings.Contains(unescaped, q) {
+ t.Errorf("Expected query string %s to contain %s", unescaped, q)
+ }
+ }
+}
diff --git a/qstring.go b/qstring.go
new file mode 100644
index 0000000..d4c2144
--- /dev/null
+++ b/qstring.go
@@ -0,0 +1,7 @@
+package qstring
+
+const (
+ // Tag indicates the name of the struct tag to extract from provided structs
+ // when marshalling or unmarshalling
+ Tag = "qstring"
+)
diff --git a/utils.go b/utils.go
new file mode 100644
index 0000000..492f773
--- /dev/null
+++ b/utils.go
@@ -0,0 +1,24 @@
+package qstring
+
+import (
+ "reflect"
+)
+
+// isEmptyValue returns true if the provided reflect.Value
+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()
+ }
+ return false
+}
diff --git a/utils_test.go b/utils_test.go
new file mode 100644
index 0000000..d115c45
--- /dev/null
+++ b/utils_test.go
@@ -0,0 +1,39 @@
+package qstring
+
+import (
+ "reflect"
+ "testing"
+)
+
+func TestIsEmptyValue(t *testing.T) {
+ var ts *TestStruct
+ ts = nil
+ testIO := []struct {
+ inp reflect.Value
+ expected bool
+ }{
+ {inp: reflect.ValueOf([]int{0}), expected: false},
+ {inp: reflect.ValueOf([]int{}), expected: true},
+ {inp: reflect.ValueOf(map[string]int{"a": 0}), expected: false},
+ {inp: reflect.ValueOf(map[string]int{}), expected: true},
+ {inp: reflect.ValueOf(false), expected: true},
+ {inp: reflect.ValueOf(true), expected: false},
+ {inp: reflect.ValueOf(5), expected: false},
+ {inp: reflect.ValueOf(0), expected: true},
+ {inp: reflect.ValueOf(uint(5)), expected: false},
+ {inp: reflect.ValueOf(uint(0)), expected: true},
+ {inp: reflect.ValueOf(float32(5)), expected: false},
+ {inp: reflect.ValueOf(float32(0)), expected: true},
+ {inp: reflect.ValueOf(&TestStruct{}), expected: false},
+ {inp: reflect.ValueOf(ts), expected: true},
+ {inp: reflect.ValueOf(nil), expected: false},
+ }
+
+ var result bool
+ for _, test := range testIO {
+ result = isEmptyValue(test.inp)
+ if result != test.expected {
+ t.Errorf("Expected %t for input %s, got %t", test.expected, test.inp, result)
+ }
+ }
+}