Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented option for comma separated slice data #3

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 40 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
# qstring
This package provides an easy way to marshal and unmarshal url query string data to
and from structs.

This package provides an easy way to marshal and unmarshal url query string data to and from structs.

This was originally forked from [dyninc/qstring](https://github.com/dyninc/qstring) but it seems to have become inactive
and I wanted some extra features so I am now maintaining this fork.

## Installation

```bash
$ go get github.com/dyninc/qstring
$ go get github.com/Southclaws/qstring
```

## Examples
Expand All @@ -17,7 +21,7 @@ package main
import (
"net/http"

"github.com/dyninc/qstring"
"github.com/Southclaws/qstring"
)

// Query is the http request query struct.
Expand All @@ -38,10 +42,9 @@ func handler(w http.ResponseWriter, req *http.Request) {
}
```

The above example will unmarshal the query string from an http.Request and
unmarshal it into the provided struct. This means that a query of
`?names=foo&names=bar&limit=50&page=1` would be unmarshaled into a struct similar
to the following:
The above example will unmarshal the query string from an http.Request and unmarshal it into the provided struct. This
means that a query of `?names=foo&names=bar&limit=50&page=1` would be unmarshaled into a struct similar to the
following:

```go
Query{
Expand All @@ -52,19 +55,20 @@ Query{
```

### Marshalling
`qstring` also exposes two methods of Marshaling structs *into* Query parameters,
one will Marshal the provided struct into a raw query string, the other will
Marshal a struct into a `url.Values` type. Some Examples of both follow.

`qstring` also exposes two methods of Marshaling structs _into_ Query parameters, one will Marshal the provided struct
into a raw query string, the other will Marshal a struct into a `url.Values` type. Some Examples of both follow.

### Marshal Raw Query String

```go
package main

import (
"fmt"
"net/http"

"github.com/dyninc/qstring"
"github.com/Southclaws/qstring"
)

// Query is the http request query struct.
Expand All @@ -87,14 +91,15 @@ func main() {
```

### Marshal url.Values

```go
package main

import (
"fmt"
"net/http"

"github.com/dyninc/qstring"
"github.com/Southclaws/qstring"
)

// Query is the http request query struct.
Expand All @@ -117,16 +122,16 @@ func main() {
```

### Nested
In the same spirit as other Unmarshaling libraries, `qstring` allows you to
Marshal/Unmarshal nested structs

In the same spirit as other Unmarshaling libraries, `qstring` allows you to Marshal/Unmarshal nested structs

```go
package main

import (
"net/http"

"github.com/dyninc/qstring"
"github.com/Southclaws/qstring"
)

// PagingParams represents common pagination information for query strings
Expand All @@ -143,9 +148,9 @@ type Query struct {
```

### Complex Structures
Again, in the spirit of other Unmarshaling libraries, `qstring` allows for some
more complex types, such as pointers and time.Time fields. A more complete
example might look something like the following code snippet

Again, in the spirit of other Unmarshaling libraries, `qstring` allows for some more complex types, such as pointers and
time.Time fields. A more complete example might look something like the following code snippet

```go
package main
Expand All @@ -171,25 +176,29 @@ type Query struct {
```

## Additional Notes
* All Timestamps are assumed to be in RFC3339 format
* A struct field tag of `qstring` is supported and supports all of the features
you've come to know and love from Go (un)marshalers.
* A field tag with a value of `qstring:"-"` instructs `qstring` to ignore the field.
* A field tag with an the `omitempty` option set will be ignored if the field
being marshaled has a zero value. `qstring:"name,omitempty"`

- All Timestamps are assumed to be in RFC3339 format
- A struct field tag of `qstring` is supported and supports all of the features you've come to know and love from Go
(un)marshalers.
- A field tag with a value of `qstring:"-"` instructs `qstring` to ignore the field.
- A field tag with an the `omitempty` option set will be ignored if the field being marshaled has a zero value.
`qstring:"name,omitempty"`
- A slice field tag with the `comma` option set will produce a comma-separated list of items in a single query
parameter instead of multiple instances of the parameter. `qstring:"names,comma"` will produce `?names=a,b,c`
instead of `?names=a&names=b&names=c`.

### Custom Fields
In order to facilitate more complex queries `qstring` also provides some custom
fields to save you a bit of headache with custom marshal/unmarshaling logic.
Currently the following custom fields are provided:

* `qstring.ComparativeTime` - Supports timestamp query parameters with optional
logical operators (<, >, <=, >=) such as `?created<=2006-01-02T15:04:05Z`
In order to facilitate more complex queries `qstring` also provides some custom fields to save you a bit of headache
with custom marshal/unmarshaling logic. Currently the following custom fields are provided:

- `qstring.ComparativeTime` - Supports timestamp query parameters with optional logical operators (<, >, <=, >=) such as
`?created<=2006-01-02T15:04:05Z`

## Benchmarks

```
BenchmarkUnmarshall-4 500000 2711 ns/op 448 B/op 23 allocs/op
BenchmarkRawPLiteral-4 1000000 1675 ns/op 448 B/op 23 allocs/op
ok github.com/dyninc/qstring 3.163s
ok github.com/Southclaws/qstring 3.163s
```
14 changes: 10 additions & 4 deletions decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func (d *decoder) value(val reflect.Value) error {
// pull out the qstring struct tag
elemField := elem.Field(i)
typField := typ.Field(i)
qstring, _ := parseTag(typField.Tag.Get(Tag))
qstring, _, comma := parseTag(typField.Tag.Get(Tag))
if qstring == "" {
// resolvable fields must have at least the `flag` struct tag
qstring = strings.ToLower(typField.Name)
Expand All @@ -87,7 +87,7 @@ func (d *decoder) value(val reflect.Value) error {
if query, ok := d.data[qstring]; ok {
switch k := typField.Type.Kind(); k {
case reflect.Slice:
err = d.coerceSlice(query, k, elemField)
err = d.coerceSlice(query, k, elemField, comma)
default:
err = d.coerce(query[0], k, elemField)
}
Expand Down Expand Up @@ -164,15 +164,21 @@ func (d *decoder) coerce(query string, target reflect.Kind, field reflect.Value)
// 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 {
func (d *decoder) coerceSlice(query []string, target reflect.Kind, field reflect.Value, comma bool) 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 {
var elements []string
if comma && len(query) > 0 {
elements = strings.Split(query[0], ",")
} else {
elements = query
}
for _, q := range elements {
val := reflect.New(sliceType).Elem()
err = d.coerce(q, coerceKind, val)
if err != nil {
Expand Down
106 changes: 71 additions & 35 deletions decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,21 @@ type TestStruct struct {
Float64 float64

// slice fields
Fields []string `qstring:"fields"`
DoFields []bool `qstring:"dofields"`
Counts []int
IDs []int8
Smalls []int16
Meds []int32
Bigs []int64
Fields []string `qstring:"fields"`
DoFields []bool `qstring:"dofields"`
Counts []int
IDs []int8
Smalls []int16
Meds []int32
Bigs []int64
FieldsC []string `qstring:"fieldsc,comma"`
DoFieldsC []bool `qstring:"dofieldsc,comma"`
CountsC []int `qstring:",comma"`
IDsC []int8 `qstring:",comma"`
SmallsC []int16 `qstring:",comma"`
MedsC []int32 `qstring:",comma"`
BigsC []int64 `qstring:",comma"`
Float32sC []float32 `qstring:",comma"`

// uint fields
UPages []uint
Expand All @@ -55,34 +63,42 @@ type TestStruct struct {
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"},
"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"},
"fieldsc": []string{"foo,bar"},
"dofieldsc": []string{"true,false"},
"countsc": []string{"1,2"},
"idsc": []string{"3,4,5"},
"smallsc": []string{"6,7,8"},
"medsc": []string{"9,10,11"},
"bigsc": []string{"12,13,14"},
"float32sc": []string{"1.1,2.2,3.3"},
"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)
Expand All @@ -97,6 +113,26 @@ func TestUnmarshall(t *testing.T) {
if len(ts.Fields) != 2 {
t.Errorf("Expected 2 fields, got %d", len(ts.Fields))
}

if len(ts.FieldsC) != 2 {
t.Errorf("Expected 2 fields, got %d", len(ts.FieldsC))
}

if len(ts.DoFieldsC) != 2 {
t.Errorf("Expected 2 fields, got %d", len(ts.DoFieldsC))
}

if len(ts.CountsC) != 2 {
t.Errorf("Expected 2 fields, got %d", len(ts.CountsC))
}

if len(ts.IDsC) != 3 {
t.Errorf("Expected 3 fields, got %d", len(ts.IDsC))
}

if len(ts.Float32sC) != 3 {
t.Errorf("Expected 3 fields, got %d", len(ts.IDsC))
}
}

func TestUnmarshalNested(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion doc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"os"
"time"

"github.com/dyninc/qstring"
"github.com/Southclaws/qstring"
)

func ExampleUnmarshal() {
Expand Down
9 changes: 6 additions & 3 deletions encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func (e *encoder) value(val reflect.Value) (url.Values, error) {
// pull out the qstring struct tag
elemField := elem.Field(i)
typField := typ.Field(i)
qstring, omit := parseTag(typField.Tag.Get(Tag))
qstring, omit, comma := parseTag(typField.Tag.Get(Tag))
if qstring == "" {
// resolvable fields must have at least the `flag` struct tag
qstring = strings.ToLower(typField.Name)
Expand All @@ -98,7 +98,7 @@ func (e *encoder) value(val reflect.Value) (url.Values, error) {
default:
output.Set(qstring, marshalValue(elemField, k))
case reflect.Slice:
output[qstring] = marshalSlice(elemField)
output[qstring] = marshalSlice(elemField, comma)
case reflect.Ptr:
marshalStruct(output, qstring, reflect.Indirect(elemField), k)
case reflect.Struct:
Expand All @@ -108,11 +108,14 @@ func (e *encoder) value(val reflect.Value) (url.Values, error) {
return output, err
}

func marshalSlice(field reflect.Value) []string {
func marshalSlice(field reflect.Value, comma bool) []string {
var out []string
for i := 0; i < field.Len(); i++ {
out = append(out, marshalValue(field.Index(i), field.Index(i).Kind()))
}
if comma {
return []string{strings.Join(out, ",")}
}
return out
}

Expand Down
Loading