Merge pull request 'Support filtering by nature and asset country' (#24) from filters into main
All checks were successful
Badges / coveralls (push) Successful in 33s
All checks were successful
Badges / coveralls (push) Successful in 33s
Reviewed-on: #24
This commit was merged in pull request #24.
This commit is contained in:
@@ -16,21 +16,32 @@ import (
|
|||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: once we support more brokers or exchanges we should make this parameter required and
|
var (
|
||||||
// remove/change default
|
// TODO: once we support more brokers or exchanges we should make this parameter required and
|
||||||
var platform = pflag.StringP("platform", "p", "trading212", "one of the supported platforms")
|
// remove/change default
|
||||||
|
platform = pflag.StringP("platform", "p", "trading212", "One of the supported platforms")
|
||||||
|
lang = pflag.StringP("language", "l", language.Portuguese.String(), "The 2 letter language code")
|
||||||
|
// TODO: improve documentation on selectors
|
||||||
|
selectors = pflag.StringSlice("selectors", nil, "Only process entries that conform to all the selectors:")
|
||||||
|
|
||||||
var lang = pflag.StringP("language", "l", language.Portuguese.String(), "2 letter language code")
|
readerFactories = map[string]func() internal.RecordReader{
|
||||||
|
|
||||||
var readerFactories = map[string]func() internal.RecordReader{
|
|
||||||
"trading212": func() internal.RecordReader {
|
"trading212": func() internal.RecordReader {
|
||||||
return trading212.NewRecordReader(os.Stdin, internal.NewOpenFIGI(&http.Client{Timeout: 5 * time.Second}))
|
return trading212.NewRecordReader(os.Stdin, internal.NewOpenFIGI(&http.Client{Timeout: 5 * time.Second}))
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
pflag.Parse()
|
pflag.Parse()
|
||||||
|
|
||||||
|
err := run(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("found a fatal issue", slog.Any("err", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(ctx context.Context) error {
|
||||||
if platform == nil || len(*platform) == 0 {
|
if platform == nil || len(*platform) == 0 {
|
||||||
slog.Error("--platform flag is required")
|
slog.Error("--platform flag is required")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -41,14 +52,6 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := run(context.Background(), *platform, *lang)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("found a fatal issue", slog.Any("err", err))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func run(ctx context.Context, platform, lang string) error {
|
|
||||||
ctx, cancel := signal.NotifyContext(ctx, os.Kill, os.Interrupt)
|
ctx, cancel := signal.NotifyContext(ctx, os.Kill, os.Interrupt)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -56,25 +59,30 @@ func run(ctx context.Context, platform, lang string) error {
|
|||||||
|
|
||||||
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, nil)))
|
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, nil)))
|
||||||
|
|
||||||
factory, ok := readerFactories[platform]
|
factory, ok := readerFactories[*platform]
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("unsupported platform: %s", platform)
|
return fmt.Errorf("unsupported platform: %s", *platform)
|
||||||
}
|
}
|
||||||
|
|
||||||
reader := factory()
|
reader := factory()
|
||||||
|
|
||||||
writer := internal.NewAggregatorWriter()
|
writer := internal.NewAggregatorWriter()
|
||||||
|
|
||||||
|
selector, err := internal.ParseSelectors(*selectors)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parsing selectors: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
eg.Go(func() error {
|
eg.Go(func() error {
|
||||||
return internal.BuildReport(ctx, reader, writer)
|
return internal.BuildReport(ctx, reader, writer, selector)
|
||||||
})
|
})
|
||||||
|
|
||||||
err := eg.Wait()
|
err = eg.Wait()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
loc, err := NewLocalizer(lang)
|
loc, err := NewLocalizer(*lang)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create localizer: %w", err)
|
return fmt.Errorf("create localizer: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,17 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (n Nature) String() string {
|
func (n Nature) String() string {
|
||||||
if n == "" {
|
if n.Valid() {
|
||||||
return "unknown"
|
|
||||||
}
|
|
||||||
return string(n)
|
return string(n)
|
||||||
|
}
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n Nature) Valid() bool {
|
||||||
|
switch n {
|
||||||
|
case NatureG01, NatureG20:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,11 @@ func TestNature_String(t *testing.T) {
|
|||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "return unknown",
|
name: "return unknown for empty",
|
||||||
|
want: "unknown",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "return unknown for bad value",
|
||||||
want: "unknown",
|
want: "unknown",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -50,7 +50,12 @@ type ReportWriter interface {
|
|||||||
Write(context.Context, ReportItem) error
|
Write(context.Context, ReportItem) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func BuildReport(ctx context.Context, reader RecordReader, writer ReportWriter) error {
|
// Selector returns true if a record should be selected for processing, false otherwise.
|
||||||
|
type Selector func(Record) bool
|
||||||
|
|
||||||
|
// BuildReport reads records from a RecordReader and, if the record passes the Selector, it is
|
||||||
|
// processed into the report
|
||||||
|
func BuildReport(ctx context.Context, reader RecordReader, writer ReportWriter, s Selector) error {
|
||||||
buys := make(map[string]*FillerQueue)
|
buys := make(map[string]*FillerQueue)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@@ -66,6 +71,10 @@ func BuildReport(ctx context.Context, reader RecordReader, writer ReportWriter)
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !s(rec) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
buyQueue, ok := buys[rec.Symbol()]
|
buyQueue, ok := buys[rec.Symbol()]
|
||||||
if !ok {
|
if !ok {
|
||||||
buyQueue = new(FillerQueue)
|
buyQueue = new(FillerQueue)
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ func TestBuildReport(t *testing.T) {
|
|||||||
Taxes: decimal.Decimal{},
|
Taxes: decimal.Decimal{},
|
||||||
})).Times(1)
|
})).Times(1)
|
||||||
|
|
||||||
gotErr := internal.BuildReport(t.Context(), reader, writer)
|
gotErr := internal.BuildReport(t.Context(), reader, writer, internal.Any())
|
||||||
if gotErr != nil {
|
if gotErr != nil {
|
||||||
t.Fatalf("got unexpected err: %v", gotErr)
|
t.Fatalf("got unexpected err: %v", gotErr)
|
||||||
}
|
}
|
||||||
|
|||||||
114
internal/selectors.go
Normal file
114
internal/selectors.go
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Any() Selector {
|
||||||
|
return func(r Record) bool { return true }
|
||||||
|
}
|
||||||
|
|
||||||
|
func And(a, b Selector) Selector {
|
||||||
|
return func(r Record) bool {
|
||||||
|
return a(r) && b(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func OnlyNature(n Nature) Selector {
|
||||||
|
return func(r Record) bool {
|
||||||
|
return r.Nature() == n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func OnlyAssetCountry(c int64) Selector {
|
||||||
|
return func(r Record) bool {
|
||||||
|
return r.AssetCountry() == c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// selectorParser is a function that parses a selector value string into a Selector
|
||||||
|
type selectorParser func(string) (Selector, error)
|
||||||
|
|
||||||
|
// parsers maps selector keys to their parser functions
|
||||||
|
var parsers = map[string]selectorParser{
|
||||||
|
"code": parseNature,
|
||||||
|
"assetCountry": parseAssetCountry,
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseNature(value string) (Selector, error) {
|
||||||
|
if value == "" {
|
||||||
|
return nil, fmt.Errorf("code selector requires a non-empty value")
|
||||||
|
}
|
||||||
|
nature := Nature(value)
|
||||||
|
if !nature.Valid() {
|
||||||
|
return nil, fmt.Errorf("invalid nature code %q", value)
|
||||||
|
}
|
||||||
|
return OnlyNature(nature), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAssetCountry(value string) (Selector, error) {
|
||||||
|
if value == "" {
|
||||||
|
return nil, fmt.Errorf("assetCountry selector requires a non-empty value")
|
||||||
|
}
|
||||||
|
i, err := strconv.ParseInt(value, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("assetCountry value must be a valid integer: %w", err)
|
||||||
|
}
|
||||||
|
return OnlyAssetCountry(i), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseSelectors parses a list of selector strings into a composed Selector.
|
||||||
|
// Each selector string must be in the format "key:value" where key is one of:
|
||||||
|
// - code: filter by nature (e.g., "code:G01")
|
||||||
|
// - assetCountry: filter by asset country code (e.g., "assetCountry:840")
|
||||||
|
//
|
||||||
|
// Multiple selectors are combined with AND logic.
|
||||||
|
// If no selectors are provided, returns Any() which matches all records.
|
||||||
|
func ParseSelectors(sl []string) (Selector, error) {
|
||||||
|
if len(sl) == 0 {
|
||||||
|
return Any(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the first selector
|
||||||
|
first, err := parseSingleSelector(sl[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's only one, return it
|
||||||
|
if len(sl) == 1 {
|
||||||
|
return first, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively parse the rest and combine with AND
|
||||||
|
rest, err := ParseSelectors(sl[1:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return And(first, rest), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSingleSelector(s string) (Selector, error) {
|
||||||
|
key, value, found := strings.Cut(s, ":")
|
||||||
|
if !found {
|
||||||
|
return nil, fmt.Errorf("invalid selector format %q: must be 'key:value'", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
parser, ok := parsers[key]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unknown selector key %q: supported keys are %v", key, supportedKeys())
|
||||||
|
}
|
||||||
|
|
||||||
|
return parser(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func supportedKeys() []string {
|
||||||
|
keys := make([]string, 0, len(parsers))
|
||||||
|
for k := range parsers {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}
|
||||||
434
internal/selectors_test.go
Normal file
434
internal/selectors_test.go
Normal file
@@ -0,0 +1,434 @@
|
|||||||
|
package internal_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/nmoniz/any2anexoj/internal"
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testRecord struct {
|
||||||
|
symbol string
|
||||||
|
nature internal.Nature
|
||||||
|
brokerCountry int64
|
||||||
|
assetCountry int64
|
||||||
|
side internal.Side
|
||||||
|
price decimal.Decimal
|
||||||
|
quantity decimal.Decimal
|
||||||
|
timestamp time.Time
|
||||||
|
fees decimal.Decimal
|
||||||
|
taxes decimal.Decimal
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m testRecord) Symbol() string { return m.symbol }
|
||||||
|
func (m testRecord) Nature() internal.Nature { return m.nature }
|
||||||
|
func (m testRecord) BrokerCountry() int64 { return m.brokerCountry }
|
||||||
|
func (m testRecord) AssetCountry() int64 { return m.assetCountry }
|
||||||
|
func (m testRecord) Side() internal.Side { return m.side }
|
||||||
|
func (m testRecord) Price() decimal.Decimal { return m.price }
|
||||||
|
func (m testRecord) Quantity() decimal.Decimal { return m.quantity }
|
||||||
|
func (m testRecord) Timestamp() time.Time { return m.timestamp }
|
||||||
|
func (m testRecord) Fees() decimal.Decimal { return m.fees }
|
||||||
|
func (m testRecord) Taxes() decimal.Decimal { return m.taxes }
|
||||||
|
|
||||||
|
func TestAny(t *testing.T) {
|
||||||
|
selector := internal.Any()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
record internal.Record
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "returns true for any record",
|
||||||
|
record: testRecord{
|
||||||
|
symbol: "AAPL",
|
||||||
|
nature: internal.NatureG01,
|
||||||
|
assetCountry: 1,
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "returns true for record with unknown nature",
|
||||||
|
record: testRecord{
|
||||||
|
symbol: "MSFT",
|
||||||
|
nature: internal.NatureUnknown,
|
||||||
|
assetCountry: 2,
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "returns true for empty record",
|
||||||
|
record: testRecord{},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := selector(tt.record)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Fatalf("want %v but got %v", tt.want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOnlyNature(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
nature internal.Nature
|
||||||
|
record internal.Record
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "matches G01 nature",
|
||||||
|
nature: internal.NatureG01,
|
||||||
|
record: testRecord{
|
||||||
|
symbol: "AAPL",
|
||||||
|
nature: internal.NatureG01,
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "matches G20 nature",
|
||||||
|
nature: internal.NatureG20,
|
||||||
|
record: testRecord{
|
||||||
|
symbol: "ETF",
|
||||||
|
nature: internal.NatureG20,
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "does not match different nature",
|
||||||
|
nature: internal.NatureG01,
|
||||||
|
record: testRecord{
|
||||||
|
symbol: "AAPL",
|
||||||
|
nature: internal.NatureG20,
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "does not match unknown nature",
|
||||||
|
nature: internal.NatureG01,
|
||||||
|
record: testRecord{
|
||||||
|
symbol: "AAPL",
|
||||||
|
nature: internal.NatureUnknown,
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
selector := internal.OnlyNature(tt.nature)
|
||||||
|
got := selector(tt.record)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Fatalf("want %v but got %v", tt.want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOnlyAssetCountry(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
country int64
|
||||||
|
record internal.Record
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "matches asset country",
|
||||||
|
country: 840, // USA
|
||||||
|
record: testRecord{
|
||||||
|
symbol: "AAPL",
|
||||||
|
assetCountry: 840,
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "matches different country",
|
||||||
|
country: 826, // UK
|
||||||
|
record: testRecord{
|
||||||
|
symbol: "BP",
|
||||||
|
assetCountry: 826,
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "does not match different asset country",
|
||||||
|
country: 840,
|
||||||
|
record: testRecord{
|
||||||
|
symbol: "BP",
|
||||||
|
assetCountry: 826,
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "does not match zero country",
|
||||||
|
country: 0,
|
||||||
|
record: testRecord{
|
||||||
|
symbol: "AAPL",
|
||||||
|
assetCountry: 840,
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
selector := internal.OnlyAssetCountry(tt.country)
|
||||||
|
got := selector(tt.record)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Fatalf("want %v but got %v", tt.want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnd(t *testing.T) {
|
||||||
|
record := testRecord{}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
selectorA internal.Selector
|
||||||
|
selectorB internal.Selector
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "both selectors return true",
|
||||||
|
selectorA: func(internal.Record) bool { return true },
|
||||||
|
selectorB: func(internal.Record) bool { return true },
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "first selector returns true, second returns false",
|
||||||
|
selectorA: func(internal.Record) bool { return true },
|
||||||
|
selectorB: func(internal.Record) bool { return false },
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "first selector returns false, second returns true",
|
||||||
|
selectorA: func(internal.Record) bool { return false },
|
||||||
|
selectorB: func(internal.Record) bool { return true },
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "both selectors return false",
|
||||||
|
selectorA: func(internal.Record) bool { return false },
|
||||||
|
selectorB: func(internal.Record) bool { return false },
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
selector := internal.And(tt.selectorA, tt.selectorB)
|
||||||
|
got := selector(record)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Fatalf("want %v but got %v", tt.want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseSelectors_Empty(t *testing.T) {
|
||||||
|
selector, err := internal.ParseSelectors([]string{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error for empty selectors: %v", err)
|
||||||
|
}
|
||||||
|
record := testRecord{
|
||||||
|
nature: internal.NatureG01,
|
||||||
|
assetCountry: 840,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !selector(record) {
|
||||||
|
t.Fatalf("empty selectors should match all records")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseSelectors_OnlyNature(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
selector string
|
||||||
|
nature internal.Nature
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "matches G01",
|
||||||
|
selector: "code:G01",
|
||||||
|
nature: internal.NatureG01,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "matches G20",
|
||||||
|
selector: "code:G20",
|
||||||
|
nature: internal.NatureG20,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "does not match different nature",
|
||||||
|
selector: "code:G01",
|
||||||
|
nature: internal.NatureG20,
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
selector, err := internal.ParseSelectors([]string{tt.selector})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
record := testRecord{
|
||||||
|
nature: tt.nature,
|
||||||
|
assetCountry: 840,
|
||||||
|
}
|
||||||
|
|
||||||
|
got := selector(record)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Fatalf("want %v but got %v", tt.want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseSelectors_OnlyAssetCountry(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
selector string
|
||||||
|
assetCountry int64
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "matches USA",
|
||||||
|
selector: "assetCountry:840",
|
||||||
|
assetCountry: 840,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "matches UK",
|
||||||
|
selector: "assetCountry:826",
|
||||||
|
assetCountry: 826,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "does not match different country",
|
||||||
|
selector: "assetCountry:840",
|
||||||
|
assetCountry: 826,
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
selector, err := internal.ParseSelectors([]string{tt.selector})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
record := testRecord{
|
||||||
|
nature: internal.NatureG01,
|
||||||
|
assetCountry: tt.assetCountry,
|
||||||
|
}
|
||||||
|
|
||||||
|
got := selector(record)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Fatalf("want %v but got %v", tt.want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseSelectors_Multiple(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
selectors []string
|
||||||
|
nature internal.Nature
|
||||||
|
assetCountry int64
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "both selectors match",
|
||||||
|
selectors: []string{"code:G01", "assetCountry:840"},
|
||||||
|
nature: internal.NatureG01,
|
||||||
|
assetCountry: 840,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "first selector matches, second does not",
|
||||||
|
selectors: []string{"code:G01", "assetCountry:826"},
|
||||||
|
nature: internal.NatureG01,
|
||||||
|
assetCountry: 840,
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "first selector does not match, second does",
|
||||||
|
selectors: []string{"code:G20", "assetCountry:840"},
|
||||||
|
nature: internal.NatureG01,
|
||||||
|
assetCountry: 840,
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
selector, err := internal.ParseSelectors(tt.selectors)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
record := testRecord{
|
||||||
|
nature: tt.nature,
|
||||||
|
assetCountry: tt.assetCountry,
|
||||||
|
}
|
||||||
|
|
||||||
|
got := selector(record)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Fatalf("want %v but got %v", tt.want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseSelectors_InvalidAssetCountry(t *testing.T) {
|
||||||
|
t.Run("returns error for non-numeric asset country", func(t *testing.T) {
|
||||||
|
_, err := internal.ParseSelectors([]string{"assetCountry:notanumber"})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error for non-numeric asset country")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseSelectors_UnknownSelector(t *testing.T) {
|
||||||
|
t.Run("returns error for unknown selector type", func(t *testing.T) {
|
||||||
|
_, err := internal.ParseSelectors([]string{"unknown:value"})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error for unknown selector type")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseSelectors_EmptyValue(t *testing.T) {
|
||||||
|
t.Run("rejects code with empty value", func(t *testing.T) {
|
||||||
|
_, err := internal.ParseSelectors([]string{"code:"})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error for empty code value")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("rejects assetCountry with empty value", func(t *testing.T) {
|
||||||
|
_, err := internal.ParseSelectors([]string{"assetCountry:"})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error for empty assetCountry value")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseSelectors_MissingColon(t *testing.T) {
|
||||||
|
t.Run("rejects input without colon", func(t *testing.T) {
|
||||||
|
_, err := internal.ParseSelectors([]string{"code"})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error for input without colon")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user