mirror of
https://github.com/ggicci/httpin.git
synced 2025-02-21 19:06:46 +02:00
Merge pull request #107 from alecsammon/omitempty
Add support for `omitempty` field tag to exclude empty values from queries and headers
This commit is contained in:
commit
eed0621e7f
@ -52,10 +52,11 @@ Since v0.15.0, httpin also supports creating an HTTP request (`http.Request`) fr
|
|||||||
|
|
||||||
```go
|
```go
|
||||||
type ListUsersInput struct {
|
type ListUsersInput struct {
|
||||||
Token string `in:"query=access_token;header=x-access-token"`
|
Token string `in:"query=access_token;header=x-access-token"`
|
||||||
Page int `in:"query=page;default=1"`
|
Page int `in:"query=page;default=1"`
|
||||||
PerPage int `in:"query=per_page;default=20"`
|
PerPage int `in:"query=per_page;default=20"`
|
||||||
IsMember bool `in:"query=is_member"`
|
IsMember bool `in:"query=is_member"`
|
||||||
|
Search *string `in:"query=search;omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListUsers(rw http.ResponseWriter, r *http.Request) {
|
func ListUsers(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -17,6 +17,7 @@ func init() {
|
|||||||
RegisterDirective("default", &DirectiveDefault{})
|
RegisterDirective("default", &DirectiveDefault{})
|
||||||
RegisterDirective("nonzero", &DirectiveNonzero{})
|
RegisterDirective("nonzero", &DirectiveNonzero{})
|
||||||
registerDirective("path", defaultPathDirective)
|
registerDirective("path", defaultPathDirective)
|
||||||
|
registerDirective("omitempty", &DirectiveOmitEmpty{})
|
||||||
|
|
||||||
// decoder is a special executor which does nothing, but is an indicator of
|
// decoder is a special executor which does nothing, but is an indicator of
|
||||||
// overriding the decoder for a specific field.
|
// overriding the decoder for a specific field.
|
||||||
|
@ -1,12 +1,20 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import "github.com/ggicci/httpin/internal"
|
import (
|
||||||
|
"github.com/ggicci/httpin/internal"
|
||||||
|
)
|
||||||
|
|
||||||
type FormEncoder struct {
|
type FormEncoder struct {
|
||||||
Setter func(key string, value []string) // form value setter
|
Setter func(key string, value []string) // form value setter
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *FormEncoder) Execute(rtm *DirectiveRuntime) error {
|
func (e *FormEncoder) Execute(rtm *DirectiveRuntime) error {
|
||||||
|
if rtm.Value.IsZero() {
|
||||||
|
if rtm.Resolver.GetDirective("omitempty") != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if rtm.IsFieldSet() {
|
if rtm.IsFieldSet() {
|
||||||
return nil // skip when already encoded by former directives
|
return nil // skip when already encoded by former directives
|
||||||
}
|
}
|
||||||
|
@ -29,23 +29,47 @@ func TestDirectiveHeader_Decode(t *testing.T) {
|
|||||||
|
|
||||||
func TestDirectiveHeader_NewRequest(t *testing.T) {
|
func TestDirectiveHeader_NewRequest(t *testing.T) {
|
||||||
type ApiQuery struct {
|
type ApiQuery struct {
|
||||||
ApiUid int `in:"header=x-api-uid"`
|
ApiUid int `in:"header=x-api-uid;omitempty"`
|
||||||
ApiToken string `in:"header=X-Api-Token"`
|
ApiToken *string `in:"header=X-Api-Token;omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
query := &ApiQuery{
|
t.Run("with all values", func(t *testing.T) {
|
||||||
ApiUid: 91241844,
|
tk := "some-secret-token"
|
||||||
ApiToken: "some-secret-token",
|
query := &ApiQuery{
|
||||||
}
|
ApiUid: 91241844,
|
||||||
|
ApiToken: &tk,
|
||||||
|
}
|
||||||
|
|
||||||
co, err := New(ApiQuery{})
|
co, err := New(ApiQuery{})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
req, err := co.NewRequest("POST", "/api", query)
|
req, err := co.NewRequest("POST", "/api", query)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
expected, _ := http.NewRequest("POST", "/api", nil)
|
expected, _ := http.NewRequest("POST", "/api", nil)
|
||||||
// NOTE: the key will be canonicalized
|
// NOTE: the key will be canonicalized
|
||||||
expected.Header.Set("x-api-uid", "91241844")
|
expected.Header.Set("x-api-uid", "91241844")
|
||||||
expected.Header.Set("X-Api-Token", "some-secret-token")
|
expected.Header.Set("X-Api-Token", "some-secret-token")
|
||||||
assert.Equal(t, expected, req)
|
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)
|
||||||
|
|
||||||
|
_, ok = req.Header["X-Api-Token"]
|
||||||
|
assert.False(t, ok)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
17
core/omitempty.go
Normal file
17
core/omitempty.go
Normal file
@ -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
|
||||||
|
}
|
@ -33,7 +33,7 @@ func TestDirectiveQuery_Decode(t *testing.T) {
|
|||||||
func TestDirectiveQuery_NewRequest(t *testing.T) {
|
func TestDirectiveQuery_NewRequest(t *testing.T) {
|
||||||
type SearchQuery struct {
|
type SearchQuery struct {
|
||||||
Name string `in:"query=name"`
|
Name string `in:"query=name"`
|
||||||
Age int `in:"query=age"`
|
Age int `in:"query=age;omitempty"`
|
||||||
Enabled bool `in:"query=enabled"`
|
Enabled bool `in:"query=enabled"`
|
||||||
Price float64 `in:"query=price"`
|
Price float64 `in:"query=price"`
|
||||||
|
|
||||||
@ -41,42 +41,60 @@ func TestDirectiveQuery_NewRequest(t *testing.T) {
|
|||||||
AgeList []int `in:"query=age_list[]"`
|
AgeList []int `in:"query=age_list[]"`
|
||||||
|
|
||||||
NamePointer *string `in:"query=name_pointer"`
|
NamePointer *string `in:"query=name_pointer"`
|
||||||
AgePointer *int `in:"query=age_pointer"`
|
AgePointer *int `in:"query=age_pointer;omitempty"`
|
||||||
}
|
|
||||||
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{})
|
t.Run("with all values", func(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
query := &SearchQuery{
|
||||||
req, err := co.NewRequest("GET", "/pets", query)
|
Name: "cupcake",
|
||||||
assert.NoError(t, err)
|
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
|
||||||
|
}(),
|
||||||
|
}
|
||||||
|
|
||||||
expected, _ := http.NewRequest("GET", "/pets", nil)
|
co, err := New(SearchQuery{})
|
||||||
expectedQuery := make(url.Values)
|
assert.NoError(t, err)
|
||||||
expectedQuery.Set("name", query.Name) // query.Name
|
req, err := co.NewRequest("GET", "/pets", query)
|
||||||
expectedQuery.Set("age", "12") // query.Age
|
assert.NoError(t, err)
|
||||||
expectedQuery.Set("enabled", "true") // query.Enabled
|
|
||||||
expectedQuery.Set("price", "6.28") // query.Price
|
expected, _ := http.NewRequest("GET", "/pets", nil)
|
||||||
expectedQuery["name_list[]"] = query.NameList // query.NameList
|
expectedQuery := make(url.Values)
|
||||||
expectedQuery["age_list[]"] = []string{"1", "2", "3"} // query.AgeList
|
expectedQuery.Set("name", query.Name) // query.Name
|
||||||
expectedQuery.Set("name_pointer", *query.NamePointer) // query.NamePointer
|
expectedQuery.Set("age", "12") // query.Age
|
||||||
expectedQuery.Set("age_pointer", "19") // query.PointerAge
|
expectedQuery.Set("enabled", "true") // query.Enabled
|
||||||
expected.URL.RawQuery = expectedQuery.Encode()
|
expectedQuery.Set("price", "6.28") // query.Price
|
||||||
assert.Equal(t, expected, req)
|
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 {
|
type Location struct {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user