diff --git a/api/spec/packages/aip/src/currencies/operations.tsp b/api/spec/packages/aip/src/currencies/operations.tsp index dfbcdfe3fd..2a9637fc39 100644 --- a/api/spec/packages/aip/src/currencies/operations.tsp +++ b/api/spec/packages/aip/src/currencies/operations.tsp @@ -17,10 +17,10 @@ namespace Currencies; */ @friendlyName("ListCurrenciesParamsFilter") model ListCurrenciesParamsFilter { - /** - * Filter currencies by type. - */ + #suppress "@openmeter/api-spec-aip/doc-decorator" "filter field" type?: CurrencyType; + #suppress "@openmeter/api-spec-aip/doc-decorator" "filter field" + code?: Common.StringFieldFilter; } interface CurrenciesOperations { @@ -35,6 +35,18 @@ interface CurrenciesOperations { list( ...Common.PagePaginationQuery, + /** + * Sort currencies returned in the response. Supported sort attributes are: + * + * - `code` (default) + * - `name` + * + * The `asc` suffix is optional as the default sort order is ascending. The `desc` + * suffix is used to specify a descending order. + */ + @query(#{ name: "sort" }) + sort?: Common.SortQuery, + /** * Filter currencies returned in the response. * diff --git a/api/v3/api.gen.go b/api/v3/api.gen.go index 81ad7a8682..a51baf9a83 100644 --- a/api/v3/api.gen.go +++ b/api/v3/api.gen.go @@ -4591,7 +4591,12 @@ type ListCreditTransactionsParamsFilter struct { // ListCurrenciesParamsFilter Filter options for listing currencies. type ListCurrenciesParamsFilter struct { - // Type Filter currencies by type. + // Code Filters on the given string field value by either exact or fuzzy match. All + // properties are optional; provide exactly one to specify the comparison. + Code *StringFieldFilter `json:"code,omitempty"` + + // Type Currency type for custom currencies. It should be a unique code but not + // conflicting with any existing standard currency codes. Type *BillingCurrencyType `json:"type,omitempty"` } @@ -5539,6 +5544,15 @@ type ListCurrenciesParams struct { // Page Determines which page of the collection to retrieve. Page *PagePaginationQuery `json:"page,omitempty"` + // Sort Sort currencies returned in the response. Supported sort attributes are: + // + // - `code` (default) + // - `name` + // + // The `asc` suffix is optional as the default sort order is ascending. The `desc` + // suffix is used to specify a descending order. + Sort *SortQuery `form:"sort,omitempty" json:"sort,omitempty"` + // Filter Filter currencies returned in the response. // // To filter currencies by type add the following query param: filter[type]=custom @@ -7871,6 +7885,14 @@ func (siw *ServerInterfaceWrapper) ListCurrencies(w http.ResponseWriter, r *http return } + // ------------- Optional query parameter "sort" ------------- + + err = runtime.BindQueryParameterWithOptions("form", false, false, "sort", r.URL.Query(), ¶ms.Sort, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "sort", Err: err}) + return + } + // ------------- Optional query parameter "filter" ------------- err = filters.Parse(r.URL.Query(), ¶ms.Filter) @@ -10707,223 +10729,223 @@ var swaggerSpec = []string{ "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", - "qOJSV1dHd76dfft/AQAA//9a3d/1JgYEAA==", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/v3/handlers/currencies/list.go b/api/v3/handlers/currencies/list.go index e605823ad2..dac824b8a4 100644 --- a/api/v3/handlers/currencies/list.go +++ b/api/v3/handlers/currencies/list.go @@ -9,11 +9,14 @@ import ( v3 "github.com/openmeterio/openmeter/api/v3" "github.com/openmeterio/openmeter/api/v3/apierrors" + "github.com/openmeterio/openmeter/api/v3/filters" + "github.com/openmeterio/openmeter/api/v3/request" "github.com/openmeterio/openmeter/api/v3/response" "github.com/openmeterio/openmeter/openmeter/currencies" "github.com/openmeterio/openmeter/pkg/framework/commonhttp" "github.com/openmeterio/openmeter/pkg/framework/transport/httptransport" "github.com/openmeterio/openmeter/pkg/pagination" + "github.com/openmeterio/openmeter/pkg/sortx" ) type ( @@ -49,17 +52,42 @@ func (h *handler) ListCurrencies() ListCurrenciesHandler { }) } - var filterType *currencies.CurrencyType - if params.Filter != nil && params.Filter.Type != nil { - ft := FromAPIBillingCurrencyType(*params.Filter.Type) - filterType = &ft + var orderBy string + var order sortx.Order + if params.Sort != nil { + sort, err := request.ParseSortBy(*params.Sort) + if err != nil { + return ListCurrenciesRequest{}, apierrors.NewBadRequestError(ctx, err, apierrors.InvalidParameters{ + {Field: "sort", Reason: err.Error(), Source: apierrors.InvalidParamSourceQuery}, + }) + } + orderBy = sort.Field + order = sort.Order.ToSortxOrder() + } + + req := ListCurrenciesRequest{ + Page: page, + Namespace: ns, + OrderBy: currencies.OrderBy(orderBy), + Order: order, + } + + if params.Filter != nil { + if params.Filter.Type != nil { + ft := FromAPIBillingCurrencyType(*params.Filter.Type) + req.FilterType = &ft + } + + code, err := filters.FromAPIFilterString(params.Filter.Code) + if err != nil { + return ListCurrenciesRequest{}, apierrors.NewBadRequestError(ctx, err, apierrors.InvalidParameters{ + {Field: "filter[code]", Reason: err.Error(), Source: apierrors.InvalidParamSourceQuery}, + }) + } + req.Code = code } - return ListCurrenciesRequest{ - Page: page, - Namespace: ns, - FilterType: filterType, - }, nil + return req, nil }, func(ctx context.Context, request ListCurrenciesRequest) (ListCurrenciesResponse, error) { result, err := h.currencyService.ListCurrencies(ctx, request) diff --git a/api/v3/openapi.yaml b/api/v3/openapi.yaml index 2a469ac32c..05868653db 100644 --- a/api/v3/openapi.yaml +++ b/api/v3/openapi.yaml @@ -333,6 +333,21 @@ paths: description: List currencies supported by the billing system. parameters: - $ref: '#/components/parameters/PagePaginationQuery' + - name: sort + in: query + required: false + description: |- + Sort currencies returned in the response. Supported sort attributes are: + + - `code` (default) + - `name` + + The `asc` suffix is optional as the default sort order is ascending. The `desc` + suffix is used to specify a descending order. + schema: + $ref: '#/components/schemas/SortQuery' + explode: false + style: form - name: filter in: query required: false @@ -8232,9 +8247,9 @@ components: type: object properties: type: - allOf: - - $ref: '#/components/schemas/BillingCurrencyType' - description: Filter currencies by type. + $ref: '#/components/schemas/BillingCurrencyType' + code: + $ref: '#/components/schemas/StringFieldFilter' additionalProperties: false description: Filter options for listing currencies. ListCustomerEntitlementAccessResponseData: diff --git a/openmeter/currencies/adapter/currencies.go b/openmeter/currencies/adapter/currencies.go index 3300604112..7a1b9b1b8c 100644 --- a/openmeter/currencies/adapter/currencies.go +++ b/openmeter/currencies/adapter/currencies.go @@ -13,9 +13,11 @@ import ( "github.com/openmeterio/openmeter/openmeter/ent/db/currencycostbasis" "github.com/openmeterio/openmeter/openmeter/ent/db/customcurrency" "github.com/openmeterio/openmeter/pkg/currencyx" + "github.com/openmeterio/openmeter/pkg/filter" "github.com/openmeterio/openmeter/pkg/framework/entutils" "github.com/openmeterio/openmeter/pkg/models" "github.com/openmeterio/openmeter/pkg/pagination" + "github.com/openmeterio/openmeter/pkg/sortx" ) var _ currencies.Adapter = (*adapter)(nil) @@ -44,8 +46,20 @@ func mapCostBasisFromDB(c *entdb.CurrencyCostBasis) currencies.CostBasis { func (a *adapter) ListCustomCurrencies(ctx context.Context, params currencies.ListCurrenciesInput) (pagination.Result[currencies.Currency], error) { return entutils.TransactingRepo(ctx, a, func(ctx context.Context, tx *adapter) (pagination.Result[currencies.Currency], error) { q := a.db.CustomCurrency.Query(). - Where(customcurrency.Namespace(params.Namespace)). - Order(entdb.Asc(customcurrency.FieldCode)) + Where(customcurrency.Namespace(params.Namespace)) + + q = filter.ApplyToQuery(q, params.Code, customcurrency.FieldCode) + + order := entutils.GetOrdering(sortx.OrderDefault) + if !params.Order.IsDefaultValue() { + order = entutils.GetOrdering(params.Order) + } + switch params.OrderBy { + case currencies.OrderByName: + q = q.Order(customcurrency.ByName(order...)) + default: + q = q.Order(customcurrency.ByCode(order...)) + } total, err := q.Count(ctx) if err != nil { diff --git a/openmeter/currencies/models.go b/openmeter/currencies/models.go index 0af86b289e..c74281d10c 100644 --- a/openmeter/currencies/models.go +++ b/openmeter/currencies/models.go @@ -7,8 +7,10 @@ import ( "github.com/alpacahq/alpacadecimal" + "github.com/openmeterio/openmeter/pkg/filter" "github.com/openmeterio/openmeter/pkg/models" "github.com/openmeterio/openmeter/pkg/pagination" + "github.com/openmeterio/openmeter/pkg/sortx" ) type Currency struct { @@ -19,6 +21,22 @@ type Currency struct { Symbol string `json:"symbol"` } +// OrderBy specifies the field to sort currencies by. +type OrderBy string + +const ( + OrderByCode OrderBy = "code" + OrderByName OrderBy = "name" +) + +func (o OrderBy) Validate() error { + switch o { + case OrderByCode, OrderByName, "": + return nil + } + return fmt.Errorf("invalid order by: %s", o) +} + var _ models.Validator = (*ListCurrenciesInput)(nil) type ListCurrenciesInput struct { @@ -28,6 +46,11 @@ type ListCurrenciesInput struct { // FilterType filters currencies by type: "custom" or "fiat". Nil means no filter. FilterType *CurrencyType `json:"filter_type,omitempty"` + // Code filters currencies by code field. Nil means no filter. + Code *filter.FilterString `json:"code,omitempty"` + + OrderBy OrderBy + Order sortx.Order } func (i ListCurrenciesInput) Validate() error { @@ -43,6 +66,16 @@ func (i ListCurrenciesInput) Validate() error { } } + if i.Code != nil { + if err := i.Code.Validate(); err != nil { + errs = append(errs, fmt.Errorf("code: %w", err)) + } + } + + if err := i.OrderBy.Validate(); err != nil { + errs = append(errs, err) + } + return errors.Join(errs...) } diff --git a/openmeter/currencies/service/service.go b/openmeter/currencies/service/service.go index 55084cbd32..c64cd500ab 100644 --- a/openmeter/currencies/service/service.go +++ b/openmeter/currencies/service/service.go @@ -3,6 +3,8 @@ package service import ( "context" "fmt" + "slices" + "strings" "time" "github.com/invopop/gobl/currency" @@ -12,6 +14,7 @@ import ( "github.com/openmeterio/openmeter/pkg/framework/transaction" "github.com/openmeterio/openmeter/pkg/models" "github.com/openmeterio/openmeter/pkg/pagination" + "github.com/openmeterio/openmeter/pkg/sortx" ) var _ currencies.CurrencyService = (*Service)(nil) @@ -55,10 +58,18 @@ func (s *Service) ListCurrencies(ctx context.Context, params currencies.ListCurr } if includeFiat { - for _, def := range lo.Filter(currency.Definitions(), func(def *currency.Def, _ int) bool { + matchCode := params.Code.LoFilterPredicate() + filteredMatchCode, err := lo.FilterErr(currency.Definitions(), func(def *currency.Def, _ int) (bool, error) { // NOTE: this filters out non-iso currencies such as crypto - return def.ISONumeric != "" - }) { + if def.ISONumeric == "" { + return false, nil + } + return matchCode(def.ISOCode.String(), 0) + }) + if err != nil { + return pagination.Result[currencies.Currency]{}, fmt.Errorf("filtering fiat currencies by code: %w", err) + } + for _, def := range filteredMatchCode { items = append(items, currencies.Currency{ Code: def.ISOCode.String(), Name: def.Name, @@ -67,6 +78,19 @@ func (s *Service) ListCurrencies(ctx context.Context, params currencies.ListCurr } } + slices.SortFunc(items, func(a, b currencies.Currency) int { + result := 0 + if params.OrderBy == currencies.OrderByName { + result = strings.Compare(a.Name, b.Name) + } else { + result = strings.Compare(a.Code, b.Code) + } + if params.Order == sortx.OrderDesc { + return -result + } + return result + }) + total := len(items) pageSize := params.Page.PageSize diff --git a/openmeter/currencies/service/service_test.go b/openmeter/currencies/service/service_test.go new file mode 100644 index 0000000000..bac811cd62 --- /dev/null +++ b/openmeter/currencies/service/service_test.go @@ -0,0 +1,223 @@ +package service + +import ( + "context" + "testing" + + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/openmeterio/openmeter/openmeter/currencies" + "github.com/openmeterio/openmeter/pkg/filter" + "github.com/openmeterio/openmeter/pkg/framework/transaction" + "github.com/openmeterio/openmeter/pkg/pagination" + "github.com/openmeterio/openmeter/pkg/sortx" +) + +// noopDriver implements transaction.Driver as a no-op for unit tests. +type noopDriver struct{} + +func (noopDriver) Commit() error { return nil } +func (noopDriver) Rollback() error { return nil } +func (noopDriver) SavePoint() error { return nil } + +// fakeAdapter implements currencies.Adapter for unit testing the service layer. +// ListCustomCurrencies applies the Code filter from params to simulate DB-level filtering. +type fakeAdapter struct { + custom []currencies.Currency +} + +func (f *fakeAdapter) Tx(ctx context.Context) (context.Context, transaction.Driver, error) { + return ctx, noopDriver{}, nil +} + +func (f *fakeAdapter) ListCustomCurrencies(_ context.Context, params currencies.ListCurrenciesInput) (pagination.Result[currencies.Currency], error) { + items := make([]currencies.Currency, 0, len(f.custom)) + for _, c := range f.custom { + if ok, _ := params.Code.Match(c.Code); ok { + items = append(items, c) + } + } + return pagination.Result[currencies.Currency]{ + Items: items, + TotalCount: len(items), + Page: params.Page, + }, nil +} + +func (f *fakeAdapter) CreateCurrency(_ context.Context, _ currencies.CreateCurrencyInput) (currencies.Currency, error) { + panic("not implemented") +} + +func (f *fakeAdapter) CreateCostBasis(_ context.Context, _ currencies.CreateCostBasisInput) (currencies.CostBasis, error) { + panic("not implemented") +} + +func (f *fakeAdapter) ListCostBases(_ context.Context, _ currencies.ListCostBasesInput) (pagination.Result[currencies.CostBasis], error) { + panic("not implemented") +} + +// newTestService creates a Service backed by a fake adapter seeded with custom currencies. +func newTestService(custom []currencies.Currency) *Service { + return New(&fakeAdapter{custom: custom}) +} + +func TestListCurrencies_CombinedPath(t *testing.T) { + customCurrency := currencies.Currency{Code: "MYCUSTOM", Name: "My Custom Currency", Symbol: "MC"} + + svc := newTestService([]currencies.Currency{customCurrency}) + + tests := []struct { + name string + input currencies.ListCurrenciesInput + wantErr bool + assertResults func(t *testing.T, result pagination.Result[currencies.Currency]) + }{ + { + name: "no filter no sort returns combined list sorted by code asc", + input: currencies.ListCurrenciesInput{ + Namespace: "test", + Page: pagination.NewPage(1, 5), + }, + assertResults: func(t *testing.T, result pagination.Result[currencies.Currency]) { + t.Helper() + require.Equal(t, 5, len(result.Items)) + for i := 1; i < len(result.Items); i++ { + assert.LessOrEqual(t, result.Items[i-1].Code, result.Items[i].Code, "items should be sorted by code asc") + } + }, + }, + { + name: "filter by single fiat code returns only that currency", + input: currencies.ListCurrenciesInput{ + Namespace: "test", + Code: &filter.FilterString{Eq: lo.ToPtr("USD")}, + }, + assertResults: func(t *testing.T, result pagination.Result[currencies.Currency]) { + t.Helper() + require.Equal(t, 1, result.TotalCount) + assert.Equal(t, "USD", result.Items[0].Code) + }, + }, + { + name: "filter by multiple fiat codes using In returns only those currencies", + input: currencies.ListCurrenciesInput{ + Namespace: "test", + Code: &filter.FilterString{In: lo.ToPtr([]string{"USD", "EUR"})}, + }, + assertResults: func(t *testing.T, result pagination.Result[currencies.Currency]) { + t.Helper() + require.Equal(t, 2, result.TotalCount) + codes := []string{result.Items[0].Code, result.Items[1].Code} + assert.ElementsMatch(t, []string{"USD", "EUR"}, codes) + }, + }, + { + name: "filter by custom currency code returns only that custom currency", + input: currencies.ListCurrenciesInput{ + Namespace: "test", + Code: &filter.FilterString{Eq: lo.ToPtr("MYCUSTOM")}, + }, + assertResults: func(t *testing.T, result pagination.Result[currencies.Currency]) { + t.Helper() + require.Equal(t, 1, result.TotalCount) + assert.Equal(t, "MYCUSTOM", result.Items[0].Code) + }, + }, + { + name: "sort by name returns items sorted by name asc", + input: currencies.ListCurrenciesInput{ + Namespace: "test", + Code: &filter.FilterString{In: lo.ToPtr([]string{"USD", "EUR", "GBP"})}, + OrderBy: currencies.OrderByName, + }, + assertResults: func(t *testing.T, result pagination.Result[currencies.Currency]) { + t.Helper() + require.Equal(t, 3, result.TotalCount) + for i := 1; i < len(result.Items); i++ { + assert.LessOrEqual(t, result.Items[i-1].Name, result.Items[i].Name, "items should be sorted by name asc") + } + }, + }, + { + name: "sort by code desc returns items sorted by code descending", + input: currencies.ListCurrenciesInput{ + Namespace: "test", + Code: &filter.FilterString{In: lo.ToPtr([]string{"USD", "EUR", "GBP"})}, + Order: sortx.OrderDesc, + }, + assertResults: func(t *testing.T, result pagination.Result[currencies.Currency]) { + t.Helper() + require.Equal(t, 3, result.TotalCount) + for i := 1; i < len(result.Items); i++ { + assert.GreaterOrEqual(t, result.Items[i-1].Code, result.Items[i].Code, "items should be sorted by code desc") + } + }, + }, + { + name: "filter by Ne excludes a single code from combined results", + input: currencies.ListCurrenciesInput{ + Namespace: "test", + Code: &filter.FilterString{Ne: lo.ToPtr("USD")}, + // Limit to known codes plus our custom one to make the assertion easy + Page: pagination.NewPage(1, 5), + }, + assertResults: func(t *testing.T, result pagination.Result[currencies.Currency]) { + t.Helper() + for _, item := range result.Items { + assert.NotEqual(t, "USD", item.Code, "USD should be excluded") + } + }, + }, + { + name: "invalid order by returns validation error", + input: currencies.ListCurrenciesInput{ + Namespace: "test", + OrderBy: currencies.OrderBy("invalid"), + }, + wantErr: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + tc.input.Namespace = "test" + result, err := svc.ListCurrencies(t.Context(), tc.input) + if tc.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + tc.assertResults(t, result) + }) + } +} + +func TestListCurrencies_CustomOnlyPath(t *testing.T) { + customCurrency := currencies.Currency{Code: "MYCUSTOM", Name: "My Custom Currency", Symbol: "MC"} + svc := newTestService([]currencies.Currency{customCurrency}) + + t.Run("filter by type custom with code filter uses custom-only fast path", func(t *testing.T) { + ft := currencies.CurrencyTypeCustom + result, err := svc.ListCurrencies(t.Context(), currencies.ListCurrenciesInput{ + Namespace: "test", + FilterType: &ft, + Code: &filter.FilterString{Eq: lo.ToPtr("MYCUSTOM")}, + }) + require.NoError(t, err) + require.Equal(t, 1, result.TotalCount) + assert.Equal(t, "MYCUSTOM", result.Items[0].Code) + }) + + t.Run("filter by type custom returns no fiat currencies", func(t *testing.T) { + ft := currencies.CurrencyTypeCustom + result, err := svc.ListCurrencies(t.Context(), currencies.ListCurrenciesInput{ + Namespace: "test", + FilterType: &ft, + }) + require.NoError(t, err) + require.Equal(t, 1, result.TotalCount) + assert.Equal(t, "MYCUSTOM", result.Items[0].Code) + }) +} diff --git a/pkg/filter/filter.go b/pkg/filter/filter.go index b57a007175..511c055ddf 100644 --- a/pkg/filter/filter.go +++ b/pkg/filter/filter.go @@ -35,6 +35,7 @@ var ( ErrFilterMultipleOperators = errors.New("filter is invalid: multiple operators are set") ErrFilterComplexityExceeded = errors.New("filter complexity exceeds maximum allowed depth") ErrFilterFormatMismatch = errors.New("filter is invalid: format mismatch") + ErrOperationNotSupported = errors.New("filter is invalid: operation not supported") ) var ( @@ -268,6 +269,80 @@ func (f FilterString) Select(field string) func(*sql.Selector) { } } +// Match reports whether the filter matches the given string value. A nil +// receiver and an empty filter match every value. Semantics mirror Select / +// SelectWhereExpr: Contains/Ncontains are case-insensitive; Like/Ilike treat +// the pattern as SQL LIKE (% and _ wildcards); Exists checks for a non-empty +// value. +func (f *FilterString) Match(value string) (bool, error) { + if f == nil || f.IsEmpty() { + return true, nil + } + return f.matches(value) +} + +// LoFilterPredicate returns an lo.Filter-compatible predicate that evaluates +// the filter against a string field value. +func (f *FilterString) LoFilterPredicate() func(value string, _ int) (bool, error) { + return func(value string, _ int) (bool, error) { return f.Match(value) } +} + +func (f FilterString) matches(value string) (bool, error) { + switch { + case f.Eq != nil: + return value == *f.Eq, nil + case f.Ne != nil: + return value != *f.Ne, nil + case f.Exists != nil: + return *f.Exists == (value != ""), nil + case f.In != nil: + return slices.Contains(*f.In, value), nil + case f.Nin != nil: + return !slices.Contains(*f.Nin, value), nil + case f.Contains != nil: + return strings.Contains(strings.ToLower(value), strings.ToLower(*f.Contains)), nil + case f.Ncontains != nil: + return !strings.Contains(strings.ToLower(value), strings.ToLower(*f.Ncontains)), nil + case f.Like != nil: + return false, ErrOperationNotSupported + case f.Nlike != nil: + return false, ErrOperationNotSupported + case f.Ilike != nil: + return false, ErrOperationNotSupported + case f.Nilike != nil: + return false, ErrOperationNotSupported + case f.Gt != nil: + return value > *f.Gt, nil + case f.Gte != nil: + return value >= *f.Gte, nil + case f.Lt != nil: + return value < *f.Lt, nil + case f.Lte != nil: + return value <= *f.Lte, nil + case f.And != nil: + for _, child := range *f.And { + if match, err := child.matches(value); err != nil { + return false, err + } else if !match { + return false, nil + } + } + return true, nil + case f.Or != nil: + var orErr error + for _, child := range *f.Or { + if match, err := child.matches(value); err != nil { + orErr = err + } else if match { + return true, nil + } + } + return false, orErr + default: + return true, nil + } +} + // FilterInteger is a filter for an integer field. type FilterInteger struct { Eq *int `json:"$eq,omitempty"` diff --git a/pkg/filter/filter_test.go b/pkg/filter/filter_test.go index bc38180385..20c8ef6b27 100644 --- a/pkg/filter/filter_test.go +++ b/pkg/filter/filter_test.go @@ -2580,3 +2580,127 @@ func TestFilterULID_Validate(t *testing.T) { }) } } + +func TestFilterString_Match(t *testing.T) { + tests := []struct { + name string + filter *filter.FilterString + value string + want bool + wantErr bool + }{ + // nil / empty filter matches everything + {name: "nil filter matches", filter: nil, value: "USD", want: true}, + {name: "empty filter matches", filter: &filter.FilterString{}, value: "USD", want: true}, + + // Eq / Ne + {name: "Eq match", filter: &filter.FilterString{Eq: lo.ToPtr("USD")}, value: "USD", want: true}, + {name: "Eq no match", filter: &filter.FilterString{Eq: lo.ToPtr("USD")}, value: "EUR", want: false}, + {name: "Ne match", filter: &filter.FilterString{Ne: lo.ToPtr("USD")}, value: "EUR", want: true}, + {name: "Ne no match", filter: &filter.FilterString{Ne: lo.ToPtr("USD")}, value: "USD", want: false}, + + // In / Nin + {name: "In match", filter: &filter.FilterString{In: lo.ToPtr([]string{"USD", "EUR"})}, value: "EUR", want: true}, + {name: "In no match", filter: &filter.FilterString{In: lo.ToPtr([]string{"USD", "EUR"})}, value: "GBP", want: false}, + {name: "Nin match", filter: &filter.FilterString{Nin: lo.ToPtr([]string{"USD", "EUR"})}, value: "GBP", want: true}, + {name: "Nin no match", filter: &filter.FilterString{Nin: lo.ToPtr([]string{"USD", "EUR"})}, value: "USD", want: false}, + + // Exists + {name: "Exists true matches non-empty", filter: &filter.FilterString{Exists: lo.ToPtr(true)}, value: "USD", want: true}, + {name: "Exists true no match empty", filter: &filter.FilterString{Exists: lo.ToPtr(true)}, value: "", want: false}, + {name: "Exists false matches empty", filter: &filter.FilterString{Exists: lo.ToPtr(false)}, value: "", want: true}, + {name: "Exists false no match non-empty", filter: &filter.FilterString{Exists: lo.ToPtr(false)}, value: "USD", want: false}, + + // Contains / Ncontains (case-insensitive) + {name: "Contains match case-insensitive", filter: &filter.FilterString{Contains: lo.ToPtr("us")}, value: "USD", want: true}, + {name: "Contains no match", filter: &filter.FilterString{Contains: lo.ToPtr("eur")}, value: "USD", want: false}, + {name: "Ncontains match", filter: &filter.FilterString{Ncontains: lo.ToPtr("eur")}, value: "USD", want: true}, + {name: "Ncontains no match", filter: &filter.FilterString{Ncontains: lo.ToPtr("us")}, value: "USD", want: false}, + + // Like / Nlike + {name: "Like percent wildcard", filter: &filter.FilterString{Like: lo.ToPtr("U%")}, value: "USD", wantErr: true}, + {name: "Like underscore wildcard", filter: &filter.FilterString{Like: lo.ToPtr("U_D")}, value: "USD", wantErr: true}, + {name: "Like no match", filter: &filter.FilterString{Like: lo.ToPtr("E%")}, value: "USD", wantErr: true}, + {name: "Nlike match", filter: &filter.FilterString{Nlike: lo.ToPtr("E%")}, value: "USD", wantErr: true}, + {name: "Nlike no match", filter: &filter.FilterString{Nlike: lo.ToPtr("U%")}, value: "USD", wantErr: true}, + + // Ilike / Nilike (case-insensitive) + {name: "Ilike case-insensitive match", filter: &filter.FilterString{Ilike: lo.ToPtr("u%")}, value: "USD", wantErr: true}, + {name: "Ilike no match", filter: &filter.FilterString{Ilike: lo.ToPtr("e%")}, value: "USD", wantErr: true}, + {name: "Nilike match", filter: &filter.FilterString{Nilike: lo.ToPtr("e%")}, value: "USD", wantErr: true}, + {name: "Nilike no match", filter: &filter.FilterString{Nilike: lo.ToPtr("u%")}, value: "USD", wantErr: true}, + + // Gt / Gte / Lt / Lte (lexicographic) + {name: "Gt match", filter: &filter.FilterString{Gt: lo.ToPtr("GBP")}, value: "USD", want: true}, + {name: "Gt no match equal", filter: &filter.FilterString{Gt: lo.ToPtr("USD")}, value: "USD", want: false}, + {name: "Gte match equal", filter: &filter.FilterString{Gte: lo.ToPtr("USD")}, value: "USD", want: true}, + {name: "Gte match greater", filter: &filter.FilterString{Gte: lo.ToPtr("EUR")}, value: "USD", want: true}, + {name: "Lt match", filter: &filter.FilterString{Lt: lo.ToPtr("USD")}, value: "EUR", want: true}, + {name: "Lt no match equal", filter: &filter.FilterString{Lt: lo.ToPtr("EUR")}, value: "EUR", want: false}, + {name: "Lte match equal", filter: &filter.FilterString{Lte: lo.ToPtr("EUR")}, value: "EUR", want: true}, + {name: "Lte match less", filter: &filter.FilterString{Lte: lo.ToPtr("USD")}, value: "EUR", want: true}, + + // And / Or + { + name: "And all match", + filter: &filter.FilterString{And: lo.ToPtr([]filter.FilterString{ + {Gte: lo.ToPtr("EUR")}, + {Lte: lo.ToPtr("USD")}, + })}, + value: "GBP", + want: true, + }, + { + name: "And one no match", + filter: &filter.FilterString{And: lo.ToPtr([]filter.FilterString{ + {Gte: lo.ToPtr("EUR")}, + {Lt: lo.ToPtr("GBP")}, + })}, + value: "USD", + want: false, + }, + { + name: "Or first match", + filter: &filter.FilterString{Or: lo.ToPtr([]filter.FilterString{ + {Eq: lo.ToPtr("USD")}, + {Eq: lo.ToPtr("EUR")}, + })}, + value: "USD", + want: true, + }, + { + name: "Or none match", + filter: &filter.FilterString{Or: lo.ToPtr([]filter.FilterString{ + {Eq: lo.ToPtr("USD")}, + {Eq: lo.ToPtr("EUR")}, + })}, + value: "GBP", + want: false, + }, + // nested And/Or + { + name: "nested And inside Or", + filter: &filter.FilterString{Or: lo.ToPtr([]filter.FilterString{ + {Eq: lo.ToPtr("USD")}, + {And: lo.ToPtr([]filter.FilterString{ + {Gte: lo.ToPtr("GBP")}, + {Lte: lo.ToPtr("GBP")}, + })}, + })}, + value: "GBP", + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.filter.Match(tt.value) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tt.want, got) + }) + } +}