Skip to content

Commit

Permalink
Merge pull request #107 from alecsammon/omitempty
Browse files Browse the repository at this point in the history
Add support for `omitempty` field tag to exclude empty values from queries and headers
  • Loading branch information
ggicci committed May 20, 2024
2 parents 6a628af + a7e1cb0 commit eed0621
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 55 deletions.
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,11 @@ Since v0.15.0, httpin also supports creating an HTTP request (`http.Request`) fr

```go
type ListUsersInput struct {
Token string `in:"query=access_token;header=x-access-token"`
Page int `in:"query=page;default=1"`
PerPage int `in:"query=per_page;default=20"`
IsMember bool `in:"query=is_member"`
Token string `in:"query=access_token;header=x-access-token"`
Page int `in:"query=page;default=1"`
PerPage int `in:"query=per_page;default=20"`
IsMember bool `in:"query=is_member"`
Search *string `in:"query=search;omitempty"`
}

func ListUsers(rw http.ResponseWriter, r *http.Request) {
Expand Down
1 change: 1 addition & 0 deletions core/directive.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ func init() {
RegisterDirective("default", &DirectiveDefault{})
RegisterDirective("nonzero", &DirectiveNonzero{})
registerDirective("path", defaultPathDirective)
registerDirective("omitempty", &DirectiveOmitEmpty{})

// decoder is a special executor which does nothing, but is an indicator of
// overriding the decoder for a specific field.
Expand Down
10 changes: 9 additions & 1 deletion core/formencoder.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
package core

import "github.com/ggicci/httpin/internal"
import (
"github.com/ggicci/httpin/internal"
)

type FormEncoder struct {
Setter func(key string, value []string) // form value setter
}

func (e *FormEncoder) Execute(rtm *DirectiveRuntime) error {
if rtm.Value.IsZero() {
if rtm.Resolver.GetDirective("omitempty") != nil {
return nil
}
}

if rtm.IsFieldSet() {
return nil // skip when already encoded by former directives
}
Expand Down
54 changes: 39 additions & 15 deletions core/header_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,23 +29,47 @@ func TestDirectiveHeader_Decode(t *testing.T) {

func TestDirectiveHeader_NewRequest(t *testing.T) {
type ApiQuery struct {
ApiUid int `in:"header=x-api-uid"`
ApiToken string `in:"header=X-Api-Token"`
ApiUid int `in:"header=x-api-uid;omitempty"`
ApiToken *string `in:"header=X-Api-Token;omitempty"`
}

query := &ApiQuery{
ApiUid: 91241844,
ApiToken: "some-secret-token",
}
t.Run("with all values", func(t *testing.T) {
tk := "some-secret-token"
query := &ApiQuery{
ApiUid: 91241844,
ApiToken: &tk,
}

co, err := New(ApiQuery{})
assert.NoError(t, err)
req, err := co.NewRequest("POST", "/api", query)
assert.NoError(t, err)
co, err := New(ApiQuery{})
assert.NoError(t, err)
req, err := co.NewRequest("POST", "/api", query)
assert.NoError(t, err)

expected, _ := http.NewRequest("POST", "/api", nil)
// NOTE: the key will be canonicalized
expected.Header.Set("x-api-uid", "91241844")
expected.Header.Set("X-Api-Token", "some-secret-token")
assert.Equal(t, expected, req)
})

t.Run("with empty value", func(t *testing.T) {
query := &ApiQuery{
ApiUid: 0,
ApiToken: nil,
}

co, err := New(ApiQuery{})
assert.NoError(t, err)
req, err := co.NewRequest("POST", "/api", query)
assert.NoError(t, err)

expected, _ := http.NewRequest("POST", "/api", nil)
assert.Equal(t, expected, req)

_, ok := req.Header["X-Api-Uid"]
assert.False(t, ok)

expected, _ := http.NewRequest("POST", "/api", nil)
// NOTE: the key will be canonicalized
expected.Header.Set("x-api-uid", "91241844")
expected.Header.Set("X-Api-Token", "some-secret-token")
assert.Equal(t, expected, req)
_, ok = req.Header["X-Api-Token"]
assert.False(t, ok)
})
}
17 changes: 17 additions & 0 deletions core/omitempty.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// directive: "omitempty"
// https://ggicci.github.io/httpin/directives/omitempty

package core

// DirectiveOmitEmpty is used with the DirectiveQuery, DirectiveForm, and DirectiveHeader to indicate that the field
// should be omitted when the value is empty.
// It does not have any affect when used by itself
type DirectiveOmitEmpty struct{}

func (*DirectiveOmitEmpty) Decode(_ *DirectiveRuntime) error {
return nil
}

func (*DirectiveOmitEmpty) Encode(_ *DirectiveRuntime) error {
return nil
}
88 changes: 53 additions & 35 deletions core/query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,50 +33,68 @@ func TestDirectiveQuery_Decode(t *testing.T) {
func TestDirectiveQuery_NewRequest(t *testing.T) {
type SearchQuery struct {
Name string `in:"query=name"`
Age int `in:"query=age"`
Age int `in:"query=age;omitempty"`
Enabled bool `in:"query=enabled"`
Price float64 `in:"query=price"`

NameList []string `in:"query=name_list[]"`
AgeList []int `in:"query=age_list[]"`

NamePointer *string `in:"query=name_pointer"`
AgePointer *int `in:"query=age_pointer"`
}
query := &SearchQuery{
Name: "cupcake",
Age: 12,
Enabled: true,
Price: 6.28,
NameList: []string{"apple", "banana", "cherry"},
AgeList: []int{1, 2, 3},
NamePointer: func() *string {
s := "pointer cupcake"
return &s
}(),
AgePointer: func() *int {
i := 19
return &i
}(),
AgePointer *int `in:"query=age_pointer;omitempty"`
}

co, err := New(SearchQuery{})
assert.NoError(t, err)
req, err := co.NewRequest("GET", "/pets", query)
assert.NoError(t, err)

expected, _ := http.NewRequest("GET", "/pets", nil)
expectedQuery := make(url.Values)
expectedQuery.Set("name", query.Name) // query.Name
expectedQuery.Set("age", "12") // query.Age
expectedQuery.Set("enabled", "true") // query.Enabled
expectedQuery.Set("price", "6.28") // query.Price
expectedQuery["name_list[]"] = query.NameList // query.NameList
expectedQuery["age_list[]"] = []string{"1", "2", "3"} // query.AgeList
expectedQuery.Set("name_pointer", *query.NamePointer) // query.NamePointer
expectedQuery.Set("age_pointer", "19") // query.PointerAge
expected.URL.RawQuery = expectedQuery.Encode()
assert.Equal(t, expected, req)
t.Run("with all values", func(t *testing.T) {
query := &SearchQuery{
Name: "cupcake",
Age: 12,
Enabled: true,
Price: 6.28,
NameList: []string{"apple", "banana", "cherry"},
AgeList: []int{1, 2, 3},
NamePointer: func() *string {
s := "pointer cupcake"
return &s
}(),
AgePointer: func() *int {
i := 19
return &i
}(),
}

co, err := New(SearchQuery{})
assert.NoError(t, err)
req, err := co.NewRequest("GET", "/pets", query)
assert.NoError(t, err)

expected, _ := http.NewRequest("GET", "/pets", nil)
expectedQuery := make(url.Values)
expectedQuery.Set("name", query.Name) // query.Name
expectedQuery.Set("age", "12") // query.Age
expectedQuery.Set("enabled", "true") // query.Enabled
expectedQuery.Set("price", "6.28") // query.Price
expectedQuery["name_list[]"] = query.NameList // query.NameList
expectedQuery["age_list[]"] = []string{"1", "2", "3"} // query.AgeList
expectedQuery.Set("name_pointer", *query.NamePointer) // query.NamePointer
expectedQuery.Set("age_pointer", "19") // query.PointerAge
expected.URL.RawQuery = expectedQuery.Encode()
assert.Equal(t, expected, req)
})

t.Run("with empty values", func(t *testing.T) {
query := &SearchQuery{}

co, err := New(SearchQuery{})
assert.NoError(t, err)
req, err := co.NewRequest("GET", "/pets", query)
assert.NoError(t, err)

assert.True(t, req.URL.Query().Has("name"))
assert.False(t, req.URL.Query().Has("age"))

assert.True(t, req.URL.Query().Has("name_pointer"))
assert.False(t, req.URL.Query().Has("age_pointer"))
})
}

type Location struct {
Expand Down

0 comments on commit eed0621

Please sign in to comment.