Skip to content
Merged
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
21 changes: 14 additions & 7 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,26 @@ jobs:
- name: Check out code into the Go module directory
uses: actions/checkout@v4

- name: Get dependencies
- name: Download dependencies
run: go mod download

- name: Verify formatting
run: |
go get -v -t -d ./...
if [ -f Gopkg.toml ]; then
curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
dep ensure
unformatted=$(gofmt -l .)
if [ -n "$unformatted" ]; then
echo "These files are not gofmt-formatted:"
echo "$unformatted"
exit 1
fi

- name: Vet
run: go vet ./...

- name: Build
run: go build -v .
run: go build -v ./...

- name: Test
run: go test -v -coverprofile=coverage.out .
run: go test -count=3 -v -coverprofile=coverage.out .

- name: Codecov
uses: codecov/codecov-action@v4
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,19 @@ values, _ := encoder.Values(query)
fmt.Println(values.Encode()) //(unescaped) output: "user[from]=1601623397728&user[verified]=true"
```

By default, it uses the brackets, add the `dot` option to
a nested struct field to scope its children with `.`:
```go
type Query struct {
User User `qs:"user,dot"`
}

values, _ := encoder.Values(query)
fmt.Println(values.Encode()) //(unescaped) output: "user.from=1601623397728&user.verified=true"
```
The `dot` option applies only to the field it is declared on; nest it again on a
deeper struct field to keep using dots (otherwise that level falls back to brackets).

### Custom Type
Implement funcs:
* `EncodeParam` to encode itself into query param.
Expand Down
31 changes: 19 additions & 12 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ Use `WithTagAlias()` func to register custom tag alias (default is `qs`)
Encoder has `.Values()` and `Encode()` functions to encode structs into url.Values.

Supported data types:
- all basic types (`bool`, `uint`, `string`, `float64`,...)
- struct
- slice/array
- pointer
- time.Time
- custom type
- all basic types (`bool`, `uint`, `string`, `float64`,...)
- struct
- slice/array
- pointer
- time.Time
- custom type

Example

Expand Down Expand Up @@ -107,7 +107,6 @@ Including the `bracket` option to signal that the multiple URL values should hav
values, _ := encoder.Values(&Query{Tags: []string{"foo","bar"}})
fmt.Println(values.Encode()) //(unescaped) output: "tags[]=foo&tags[]=bar"


The `index` option will append an index number with brackets to value name

type Query struct {
Expand All @@ -117,7 +116,6 @@ The `index` option will append an index number with brackets to value name
values, _ := encoder.Values(&Query{Tags: []string{"foo","bar"}})
fmt.Println(values.Encode()) //(unescaped) output: "tags[0]=foo&tags[1]=bar"


All nested structs are encoded including the parent value name with brackets for scoping.

type User struct {
Expand All @@ -138,6 +136,16 @@ All nested structs are encoded including the parent value name with brackets for
values, _ := encoder.Values(querys)
fmt.Println(values.Encode()) //(unescaped) output: "user[from]=1601623397728&user[verified]=true"

Add the `dot` option to a nested struct field to scope its children with dot
notation instead of brackets. The option applies only to the field it is
declared on; nest it again on a deeper struct field to keep using dots.

type Query struct {
User User `qs:"user,dot"`
}

values, _ := encoder.Values(querys)
fmt.Println(values.Encode()) //(unescaped) output: "user.from=1601623397728&user.verified=true"

Custom type
Implement `EncodeParam` to encode itself into query param.
Expand Down Expand Up @@ -177,10 +185,9 @@ Implement `IsZero` to check whether an object is zero to determine whether it sh
}
fmt.Println(values.Encode()) //(unescaped) output: "user=sonhuynh"


Limitation
- `interface`, `[]interface`, `map` are not supported yet
- `struct`, `slice`/`array` multi-level nesting are limited
- no decoder yet
- `interface`, `[]interface`, `map` are not supported yet
- `struct`, `slice`/`array` multi-level nesting are limited
- no decoder yet
*/
package qs
33 changes: 26 additions & 7 deletions encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ func (e *encoder) encodeStruct(stVal reflect.Value, values url.Values, scope []b

if cachedFlds == nil {
cachedFlds = make(cachedFields, 0, stTyp.NumField())
e.structCaching(&cachedFlds, stVal, scope)
e.structCaching(&cachedFlds, nestedFormatBracket, scope, stVal)
e.e.cache.Store(stTyp, cachedFlds)
}

Expand Down Expand Up @@ -196,7 +196,7 @@ func (e *encoder) encodeStruct(stVal reflect.Value, values url.Values, scope []b
return nil
}

func (e *encoder) structCaching(fields *cachedFields, stVal reflect.Value, scope []byte) {
func (e *encoder) structCaching(fields *cachedFields, notation nestedFormat, scope []byte, stVal reflect.Value) {

structTyp := getType(stVal)

Expand All @@ -217,10 +217,18 @@ func (e *encoder) structCaching(fields *cachedFields, stVal reflect.Value, scope

if string(scope) != "" {
scopedName := strings.Builder{}
scopedName.Write(scope)
scopedName.WriteRune('[')
scopedName.Write(e.tags[0])
scopedName.WriteRune(']')
switch notation {
case nestedFormatDot:
scopedName.Write(scope)
scopedName.WriteRune('.')
scopedName.Write(e.tags[0])
default:
scopedName.Write(scope)
scopedName.WriteRune('[')
scopedName.Write(e.tags[0])
scopedName.WriteRune(']')
}

e.tags[0] = e.tags[0][:0]
e.tags[0] = append(e.tags[0], scopedName.String()...)
}
Expand All @@ -245,11 +253,13 @@ func (e *encoder) structCaching(fields *cachedFields, stVal reflect.Value, scope
// Clear and set new scope
e.scope = e.scope[:0]
e.scope = append(e.scope, e.tags[0]...)
// How this struct's children should be scoped under its name
childNotation := nestedFormatFromOptions(e.tags[1:])
// New embed field
field := newEmbedField(fieldVal.NumField(), e.tags[0], e.tags[1:])
*fields = append(*fields, field)
// Recursive
e.structCaching(&field.cachedFields, fieldVal, e.scope)
e.structCaching(&field.cachedFields, childNotation, e.scope, fieldVal)
case reflect.Slice, reflect.Array:
//Slice element type
elemType := fieldTyp.Elem()
Expand Down Expand Up @@ -277,6 +287,15 @@ func (e *encoder) structCaching(fields *cachedFields, stVal reflect.Value, scope
}
}

func nestedFormatFromOptions(tagOptions [][]byte) nestedFormat {
for _, tagOption := range tagOptions {
if string(tagOption) == "dot" {
return nestedFormatDot
}
}
return nestedFormatBracket
}

func (e *encoder) getTagNameAndOpts(f reflect.StructField) {
// Get tag by alias
tag := f.Tag.Get(e.e.tagAlias)
Expand Down
10 changes: 8 additions & 2 deletions encode_field.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ const (
arrayFormatIndex
)

type nestedFormat uint8

const (
nestedFormatBracket nestedFormat = iota // user[from]
nestedFormatDot // user.from
)

// other fields implement baseField
type baseField struct {
name string
Expand Down Expand Up @@ -237,7 +244,7 @@ func (e *encoder) newListField(elemTyp reflect.Type, tagName []byte, tagOptions
}

if field, ok := listField.cachedField.(*embedField); ok {
e.structCaching(&field.cachedFields, reflect.Zero(elemTyp), nil)
e.structCaching(&field.cachedFields, nestedFormatBracket, nil, reflect.Zero(elemTyp))
}

return listField
Expand Down Expand Up @@ -307,7 +314,6 @@ func newMapField(keyType reflect.Type, valueType reflect.Type, tagName []byte, t
return field
}

//
type boolField struct {
*baseField
useInt bool
Expand Down
65 changes: 64 additions & 1 deletion encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ func TestGetTag3(t *testing.T) {
}

func TestEncodeInvalidValue(t *testing.T) {
//t.Parallel()
t.Parallel()

var ptr *string

Expand All @@ -223,7 +223,9 @@ func TestEncodeInvalidValue(t *testing.T) {
}

for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
encoder := NewEncoder()
_, err := encoder.Values(testCase.input)
if err == nil {
Expand Down Expand Up @@ -897,6 +899,67 @@ func TestNestedStruct(t *testing.T) {
}
}

func TestNestedStructDotNotation(t *testing.T) {
t.Parallel()
encoder := NewEncoder()

tm := time.Unix(600, 0)

type Geo struct {
Lat string `qs:"lat"`
}

type Addr struct {
City string `qs:"city"`
}

type User struct {
Verified bool `qs:"verified"`
From time.Time `qs:"from,second"`
Name *string `qs:"name,omitempty"`
Addr Addr `qs:"addr,dot"` // dot cascades only because declared again
Geo Geo `qs:"geo"` // no dot -> brackets even under a dot parent
}

s := struct {
User User `qs:"user,dot"`
UserPtr *User `qs:"user_ptr,dot"`
}{
User: User{
Verified: true,
From: tm,
Addr: Addr{City: "hcm"},
Geo: Geo{Lat: "10.5"},
},
UserPtr: &User{
Verified: false,
From: tm,
Addr: Addr{City: "hn"},
Geo: Geo{Lat: "21.0"},
},
}

values, err := encoder.Values(&s)
if err != nil {
t.Errorf("expected no error but got %v", err)
t.FailNow()
}
expected := url.Values{
"user.verified": []string{"true"},
"user.from": []string{"600"},
"user.addr.city": []string{"hcm"},
"user.geo[lat]": []string{"10.5"},
"user_ptr.verified": []string{"false"},
"user_ptr.from": []string{"600"},
"user_ptr.addr.city": []string{"hn"},
"user_ptr.geo[lat]": []string{"21.0"},
}
if !reflect.DeepEqual(expected, values) {
t.Errorf("expected %v, got %v", expected, values)
t.FailNow()
}
}

func TestEncodeInterface(t *testing.T) {
t.Parallel()
encoder := NewEncoder()
Expand Down
18 changes: 14 additions & 4 deletions example/embed/embed.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,32 @@ package main

import (
"fmt"
"github.com/sonh/qs"
"time"

"github.com/sonh/qs"
)

type User struct {
Verified bool `qs:"verified"`
From time.Time `qs:"from,millis"`
Verified bool `qs:"verified"`
From time.Time `qs:"from,millis"`
Roles map[string]int `qs:"roles"`
}

type Query struct {
User User `qs:"user"`
// The "dot" option encodes the nested struct using dot-notation keys
// (user.verified) instead of the default bracket scoping (user[verified]).
User User `qs:"user,dot"`
}

func main() {
query := Query{
User: User{
Verified: true,
From: time.Now(),
Roles: map[string]int{
"admin": 1,
"member": 2,
},
},
}

Expand All @@ -30,5 +38,7 @@ func main() {
fmt.Println("failed")
return
}
// (unescaped) output:
// user.from=1601623397728&user.roles[admin]=1&user.roles[member]=2&user.verified=true
fmt.Println(values.Encode())
}
Loading