1
0
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:
Ggicci 2024-05-20 00:32:17 -04:00 committed by GitHub
commit eed0621e7f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 123 additions and 54 deletions

View File

@ -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) {

View File

@ -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.

View File

@ -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
} }

View File

@ -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
View 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
}

View File

@ -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 {