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
47 changes: 46 additions & 1 deletion api/v3/filters/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,12 +161,25 @@ func parseFiltersValue(qs url.Values, v reflect.Value) error {
fieldVal.Set(reflect.ValueOf(&parsed))

case stringPtrType:
if hasOperatorStyleKeys(qs, name) {
return fmt.Errorf("filter[%s]: operator-style keys are not supported for this field", name)
}
if err := parseStringPtr(qs, name, fieldVal); err != nil {
return err
}

default:
return fmt.Errorf("filter[%s]: unsupported filter field type %s", name, fieldVal.Type())
// Handle *T where T is a named string-based type (e.g. *BillingCreditTransactionType).
if fieldVal.Kind() == reflect.Pointer && fieldVal.Type().Elem().Kind() == reflect.String {
if hasOperatorStyleKeys(qs, name) {
return fmt.Errorf("filter[%s]: operator-style keys are not supported for this field", name)
}
if err := parseStringPtrTyped(qs, name, fieldVal); err != nil {
return err
}
} else {
return fmt.Errorf("filter[%s]: unsupported filter field type %s", name, fieldVal.Type())
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}

Expand All @@ -192,6 +205,27 @@ func parseStringPtr(qs url.Values, name string, fieldVal reflect.Value) error {
return nil
}

// parseStringPtrTyped handles filter[field]=value for *T fields where T is a named string type.
func parseStringPtrTyped(qs url.Values, name string, fieldVal reflect.Value) error {
prefix := "filter[" + name + "]"
for key, values := range qs {
if key != prefix {
continue
}
val, err := singleValue(key, values)
if err != nil {
return err
}
if val != "" {
ptr := reflect.New(fieldVal.Type().Elem())
ptr.Elem().SetString(val)
fieldVal.Set(ptr)
}
break
}
return nil
}

// parseFilterString extracts a FilterString supporting all string operators.
func parseFilterString(qs url.Values, field string) (FilterString, error) {
var f FilterString
Expand Down Expand Up @@ -502,6 +536,17 @@ func hasFilterKeys(qs url.Values) bool {
return false
}

// hasOperatorStyleKeys reports whether qs contains any filter[field][op] keys for the given field.
func hasOperatorStyleKeys(qs url.Values, name string) bool {
prefix := "filter[" + name + "]["
for key := range qs {
if strings.HasPrefix(key, prefix) {
return true
}
}
return false
}

// hasFieldKeys reports whether qs contains filter[field] or filter[field][op] keys.
func hasFieldKeys(qs url.Values, field string) bool {
prefix := "filter[" + field + "]"
Expand Down
34 changes: 34 additions & 0 deletions api/v3/filters/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import (
"github.com/stretchr/testify/require"
)

type testStringType string

// testFilter is a target exercising every filter type Parse recognizes.
type testFilter struct {
Field *FilterString `json:"field,omitempty"`
Expand All @@ -24,6 +26,7 @@ type testFilter struct {
CreatedAt *FilterDateTime `json:"created_at,omitempty"`
Enabled *FilterBoolean `json:"enabled,omitempty"`
Currency *string `json:"currency,omitempty"`
TxType *testStringType `json:"tx_type,omitempty"`
}

func TestParse_FilterString(t *testing.T) {
Expand Down Expand Up @@ -301,6 +304,37 @@ func TestParse_StringPtr(t *testing.T) {
})
}

func TestParse_NamedStringType(t *testing.T) {
t.Run("named string type filter value", func(t *testing.T) {
var f testFilter
require.NoError(t, Parse(url.Values{"filter[tx_type]": {"funded"}}, &f))
require.NotNil(t, f.TxType)
assert.Equal(t, testStringType("funded"), *f.TxType)
})

t.Run("nil when no filter key present", func(t *testing.T) {
var f testFilter
require.NoError(t, Parse(url.Values{}, &f))
assert.Nil(t, f.TxType)
})

t.Run("operator-style key rejected", func(t *testing.T) {
var f testFilter
err := Parse(url.Values{"filter[tx_type][eq]": {"funded"}}, &f)
require.Error(t, err)
assert.Contains(t, err.Error(), "operator-style keys are not supported")
})
}

func TestParse_StringPtrOperatorRejected(t *testing.T) {
t.Run("operator-style key rejected for *string field", func(t *testing.T) {
var f testFilter
err := Parse(url.Values{"filter[currency][eq]": {"USD"}}, &f)
require.Error(t, err)
assert.Contains(t, err.Error(), "operator-style keys are not supported")
})
}

func TestParse_PointerToPointer(t *testing.T) {
t.Run("allocates pointer when filter keys exist", func(t *testing.T) {
var f *testFilter
Expand Down
Loading