From 425ef061b957c86fe4ae7218f259b18cd5a33c0b Mon Sep 17 00:00:00 2001 From: Omkar Ugalmugle Date: Sat, 11 Jul 2026 13:30:58 +0000 Subject: [PATCH 1/3] feat(filters): Support comparison operators Signed-off-by: Omkar Ugalmugle --- go.mod | 2 + go.sum | 2 - internal/resource/cmd/cmd.go | 82 ------------- internal/resource/cmd/cmd_test.go | 97 ++++++++++++++- internal/resource/cmd/filter.go | 165 ++++++++++++++++++++++++++ internal/resource/testing/resource.go | 10 +- 6 files changed, 268 insertions(+), 90 deletions(-) create mode 100644 internal/resource/cmd/filter.go diff --git a/go.mod b/go.mod index 37cf930b..79a1677f 100644 --- a/go.mod +++ b/go.mod @@ -194,3 +194,5 @@ require ( ) tool github.com/caarlos0/svu/v3 + +replace unikraft.com/x/filters => ../x/filters diff --git a/go.sum b/go.sum index cad171a8..3642b1d9 100644 --- a/go.sum +++ b/go.sum @@ -488,8 +488,6 @@ unikraft.com/cloud/sdk v0.3.1-0.20260730110715-92b5bae5afbe h1:oupb6urtot/mVqphO unikraft.com/cloud/sdk v0.3.1-0.20260730110715-92b5bae5afbe/go.mod h1:m/TX4opCAcR4OFFP/X8TWAjtzzW0Z6FxuVYznw9UW2Q= unikraft.com/x/colors v0.0.0-20260710212606-b68ba398853d h1:xNtt0WCZRm/WIr3F48AnYc7m41W6TqgvsqprDbXi4/A= unikraft.com/x/colors v0.0.0-20260710212606-b68ba398853d/go.mod h1:4jHfTebx2ff1Gd5MbS/xMrGFKRQTuwvTdzmAzaJWlSM= -unikraft.com/x/filters v0.0.0-20260416164455-ec39ae908f3f h1:v6pitpzsBnOjyzDIW0/YAEHHSI6cNOw4QOwQV0uD+dc= -unikraft.com/x/filters v0.0.0-20260416164455-ec39ae908f3f/go.mod h1:m4Qdsw8FQThJcu8g+XTEdcmpN+blf/jBuNRI81juz1M= unikraft.com/x/fingerprint v0.0.0-20260126094137-ab6e717e5679 h1:zdvJjNkjsriS8RM46FcdgcRoCh4EYM66PGjqVgi/ups= unikraft.com/x/fingerprint v0.0.0-20260126094137-ab6e717e5679/go.mod h1:FP7uOxux/W5PKqSRQsR4tyjNuLq4Cfio7mc5QVH1kW8= unikraft.com/x/guesstermwidth v0.0.0-20260708141520-0cbb0a92056b h1:/VC/nXdu6k0G+a9Lkd9P/sz1r1mrlh0rocNtr1M84V8= diff --git a/internal/resource/cmd/cmd.go b/internal/resource/cmd/cmd.go index 928f938f..987c2a49 100644 --- a/internal/resource/cmd/cmd.go +++ b/internal/resource/cmd/cmd.go @@ -14,13 +14,10 @@ import ( "fmt" "io" "os" - "reflect" "slices" - "strconv" "strings" "time" - "github.com/charmbracelet/x/ansi" "unikraft.com/x/filters" "unikraft.com/x/kingkong" "unikraft.com/x/log" @@ -29,7 +26,6 @@ import ( "unikraft.com/cli/internal/multimetro" "unikraft.com/cli/internal/resource" "unikraft.com/cli/internal/resource/patch" - "unikraft.com/cli/internal/resource/value" "unikraft.com/cli/internal/tui/watcher" xkong "unikraft.com/cli/internal/x/kong" "unikraft.com/cloud/sdk/platform/group" @@ -412,84 +408,6 @@ func filterResources(ctx context.Context, resources []resource.Resource, filter return filtered, rerr } -// newFieldAdaptor creates a filters.Adaptor that can traverse resource fields. -// It handles both structured fields (with subfields) and slice values (like []string tags). -func newFieldAdaptor(fields []resource.Field) filters.AdapterFunc { - return func(key []string) (string, []string, bool) { - matched := resource.GetFieldByPath(fields, key) - if len(matched) == 0 { - // GetFieldByPath may not find a match if we're looking up an index - // in a slice value (e.g., ["tags", "0"]). Try to handle this case. - if len(key) >= 2 { - parentMatched := resource.GetFieldByPath(fields, key[:len(key)-1]) - if len(parentMatched) == 1 { - if slice, ok := getSliceValue(parentMatched[0].Value); ok { - idx, err := strconv.Atoi(key[len(key)-1]) - if err == nil && idx >= 0 && idx < len(slice) { - return slice[idx], nil, true - } - } - } - } - return "", nil, false - } - if len(matched) == 1 { - field := matched[0] - // If the field has subfields, return their names as entries - // This enables wildcard filtering (e.g., nested.*.value) - if len(field.Subfields) > 0 { - entries := make([]string, len(field.Subfields)) - for i, sub := range field.Subfields { - entries[i] = sub.Name - } - return "", entries, true - } - // Check if the field's value is a slice (e.g., []string tags) - // If so, return indices as entries for wildcard support - if slice, ok := getSliceValue(field.Value); ok { - entries := make([]string, len(slice)) - for i := range slice { - entries[i] = strconv.Itoa(i) - } - return "", entries, true - } - // HACK: strip escape sequences from rendered output - out, _ := field.Render(value.RenderOpts{}) - return ansi.Strip(out), nil, true - } - // >1 fields = ambiguous match, return entries for wildcard support - entries := make([]string, len(matched)) - for i, field := range matched { - entries[i] = field.Name - } - return "", entries, true - } -} - -// getSliceValue extracts a []string from a field value if possible. -// It handles both []string and other slice types by converting to strings. -func getSliceValue(value any) ([]string, bool) { - if value == nil { - return nil, false - } - rv := reflect.ValueOf(value) - if rv.Kind() != reflect.Slice { - return nil, false - } - result := make([]string, rv.Len()) - for i := range rv.Len() { - elem := rv.Index(i) - if s, ok := elem.Interface().(string); ok { - result[i] = s - } else if s, ok := elem.Interface().(fmt.Stringer); ok { - result[i] = s.String() - } else { - result[i] = fmt.Sprintf("%v", elem.Interface()) - } - } - return result, true -} - type ResourceRemoveCmd[R resource.DeletableResource] struct { Targets []string `arg:"" name:"target" completion-predictor:"resource-key-${name}" help:"Target ${names} to remove."` diff --git a/internal/resource/cmd/cmd_test.go b/internal/resource/cmd/cmd_test.go index 025e2af9..22fcac20 100644 --- a/internal/resource/cmd/cmd_test.go +++ b/internal/resource/cmd/cmd_test.go @@ -42,8 +42,9 @@ func setupTestEnv() *resourcet.TestEnv { Hidden: "hidden-test1", Invisible: "invisible-test1", Settings: resourcet.TestSettings{ - Foo: 42, - Bar: "hello", + Foo: 42, + Bar: "hello", + Score: 10.0, }, Authors: []resourcet.TestAuthor{ {Name: "Alice", Email: "alice@example.com"}, @@ -58,8 +59,9 @@ func setupTestEnv() *resourcet.TestEnv { Hidden: "hidden-test2", Invisible: "invisible-test2", Settings: resourcet.TestSettings{ - Foo: 7, - Bar: "world", + Foo: 7, + Bar: "world", + Score: 1.2, }, Authors: []resourcet.TestAuthor{ {Name: "Charlie", Email: "charlie@example.com"}, @@ -2066,3 +2068,90 @@ func TestDeleteBulk(t *testing.T) { assert.Contains(t, env.Store, "test2") }) } + +func TestFilterComparisonOperators(t *testing.T) { + env := setupTestEnv() + ctx := resourcet.WithTestEnv(context.Background(), env) + sandbox := &resource.Sandbox{} + + runFilter := func(t *testing.T, filter string) (string, error) { + t.Helper() + var out bytes.Buffer + cmd := &ResourceListCmd[resourcet.TestResource]{ + Filter: []string{filter}, + FormatOpts: FormatOpts{ + Output: Printer{Type: PrinterTypeQuiet}, + }, + } + err := cmd.Run(ctx, testStdio(&out), sandbox) + return out.String(), err + } + + t.Run("equal", func(t *testing.T) { + output, err := runFilter(t, "settings.foo==7") + require.NoError(t, err) + assert.Contains(t, output, "test2") + assert.NotContains(t, output, "test1") + }) + + t.Run("greater", func(t *testing.T) { + output, err := runFilter(t, "settings.foo>7") + require.NoError(t, err) + assert.Contains(t, output, "test1") + assert.NotContains(t, output, "test2") + }) + + t.Run("greater_equal", func(t *testing.T) { + output, err := runFilter(t, "settings.foo>=7") + require.NoError(t, err) + assert.Contains(t, output, "test1") + assert.Contains(t, output, "test2") + }) + + t.Run("less", func(t *testing.T) { + output, err := runFilter(t, "settings.foo<42") + require.NoError(t, err) + assert.NotContains(t, output, "test1") + assert.Contains(t, output, "test2") + }) + + t.Run("less_equal", func(t *testing.T) { + output, err := runFilter(t, "settings.foo<=7") + require.NoError(t, err) + assert.NotContains(t, output, "test1") + assert.Contains(t, output, "test2") + }) + + t.Run("float_field_vs_int_literal", func(t *testing.T) { + output, err := runFilter(t, "settings.score<5") + require.NoError(t, err) + assert.Contains(t, output, "test2") + assert.NotContains(t, output, "test1") + }) + + t.Run("float_field_greater_equal", func(t *testing.T) { + output, err := runFilter(t, "settings.score>=10") + require.NoError(t, err) + assert.Contains(t, output, "test1") + assert.NotContains(t, output, "test2") + }) + + t.Run("invalid_literal_no_match_no_error", func(t *testing.T) { + output, err := runFilter(t, "settings.foo>notanumber") + require.NoError(t, err) + assert.Empty(t, strings.TrimSpace(output)) + }) + + t.Run("string_field_ordering_no_op", func(t *testing.T) { + output, err := runFilter(t, "state>pending") + require.NoError(t, err) + assert.Empty(t, strings.TrimSpace(output)) + }) + + t.Run("combined_with_equality", func(t *testing.T) { + output, err := runFilter(t, "state==pending,settings.foo>7") + require.NoError(t, err) + assert.Contains(t, output, "test1") + assert.NotContains(t, output, "test2") + }) +} diff --git a/internal/resource/cmd/filter.go b/internal/resource/cmd/filter.go new file mode 100644 index 00000000..dbc0880b --- /dev/null +++ b/internal/resource/cmd/filter.go @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025, Unikraft GmbH and The Unikraft CLI Authors. +// Licensed under the BSD-3-Clause License (the "License"). +// You may not use this file except in compliance with the License. + +package cmd + +import ( + "fmt" + "reflect" + "strconv" + "time" + + "github.com/charmbracelet/x/ansi" + + "unikraft.com/cli/internal/resource" + "unikraft.com/cli/internal/resource/value" + "unikraft.com/x/filters" +) + +type fieldAdaptor struct { + children []resource.Field + field *resource.Field + entries []string + sliceVal *string +} + +func newFieldAdaptor(fields []resource.Field) filters.Adaptor { + return &fieldAdaptor{children: fields} +} + +func (a *fieldAdaptor) Select(key []string) (filters.Adaptor, bool) { + if a.sliceVal != nil { + return nil, false + } + matched := resource.GetFieldByPath(a.children, key) + if len(matched) == 0 { + if len(key) >= 1 { + parentKey := key[:len(key)-1] + var parent *resource.Field + if len(parentKey) == 0 { + parent = a.field + } else if pm := resource.GetFieldByPath(a.children, parentKey); len(pm) == 1 { + parent = &pm[0] + } + if parent != nil { + if slice, sok := getSliceValue(parent.Value); sok { + idx, err := strconv.Atoi(key[len(key)-1]) + if err == nil && idx >= 0 && idx < len(slice) { + s := slice[idx] + return &fieldAdaptor{sliceVal: &s}, true + } + } + } + } + return nil, false + } + + if len(matched) == 1 { + f := matched[0] + if len(f.Subfields) > 0 { + names := make([]string, len(f.Subfields)) + for i, sub := range f.Subfields { + names[i] = sub.Name + } + return &fieldAdaptor{children: f.Subfields, field: &f, entries: names}, true + } + if slice, sok := getSliceValue(f.Value); sok { + names := make([]string, len(slice)) + for i := range slice { + names[i] = strconv.Itoa(i) + } + return &fieldAdaptor{field: &f, entries: names}, true + } + return &fieldAdaptor{field: &f}, true + } + + names := make([]string, len(matched)) + for i, f := range matched { + names[i] = f.Name + } + return &fieldAdaptor{entries: names}, true +} + +func (a *fieldAdaptor) String() string { + if a.sliceVal != nil { + return *a.sliceVal + } + if a.field == nil { + return "" + } + out, _ := a.field.Render(value.RenderOpts{}) + return ansi.Strip(out) +} + +func (a *fieldAdaptor) Value() any { + if a.sliceVal != nil { + return *a.sliceVal + } + if a.field == nil { + return nil + } + return a.field.Value +} + +func (a *fieldAdaptor) Entries() []string { + return a.entries +} + +func (a *fieldAdaptor) Compare(other string) (int, bool) { + if a.sliceVal != nil { + return 0, false + } + if a.field == nil || a.field.Value == nil { + return 0, false + } + if !isOrderable(a.field.Value) { + return 0, false + } + parsed, err := value.ParseNew([]string{other}, a.field.Value) + if err != nil { + return 0, false + } + return value.Compare(a.field.Value, parsed), true +} + +func isOrderable(v any) bool { + rv := reflect.ValueOf(v) + for rv.Kind() == reflect.Pointer { + if rv.IsNil() { + return false + } + rv = rv.Elem() + } + switch rv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Float32, reflect.Float64, + reflect.Bool, reflect.String: + return true + } + return rv.Type().ConvertibleTo(reflect.TypeFor[time.Time]()) +} + +func getSliceValue(value any) ([]string, bool) { + if value == nil { + return nil, false + } + rv := reflect.ValueOf(value) + if rv.Kind() != reflect.Slice { + return nil, false + } + result := make([]string, rv.Len()) + for i := range rv.Len() { + elem := rv.Index(i) + if s, ok := elem.Interface().(string); ok { + result[i] = s + } else if s, ok := elem.Interface().(fmt.Stringer); ok { + result[i] = s.String() + } else { + result[i] = fmt.Sprintf("%v", elem.Interface()) + } + } + return result, true +} diff --git a/internal/resource/testing/resource.go b/internal/resource/testing/resource.go index 3e225a7e..b0e68395 100644 --- a/internal/resource/testing/resource.go +++ b/internal/resource/testing/resource.go @@ -76,8 +76,9 @@ var ( ) type TestSettings struct { - Foo int - Bar string + Foo int + Bar string + Score float64 } type TestAuthor struct { @@ -210,6 +211,11 @@ func (t TestResource) Fields(ctx context.Context) ([]resource.Field, error) { Set: t.Settings.Bar, // Use actual value }, }, + { + Name: "score", + Value: t.Settings.Score, + Verbosity: resource.FieldVerbosityLong, + }, }, }, { From 43c73f9cf6a09390e674b0bdf82605cd56933f36 Mon Sep 17 00:00:00 2001 From: Justin Chadwell Date: Tue, 4 Aug 2026 16:36:22 +0100 Subject: [PATCH 2/3] fix(filters): Separate equality from ordering filter comparisons Signed-off-by: Justin Chadwell --- go.mod | 4 +-- go.sum | 2 ++ internal/resource/cmd/cmd_test.go | 37 +++++++++++++++++++++++++++ internal/resource/cmd/filter.go | 23 ++++++++++++----- internal/resource/testing/resource.go | 6 +++++ 5 files changed, 63 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 79a1677f..4a5cf754 100644 --- a/go.mod +++ b/go.mod @@ -51,7 +51,7 @@ require ( sigs.k8s.io/yaml v1.6.0 unikraft.com/cloud/sdk v0.3.1-0.20260730110715-92b5bae5afbe unikraft.com/x/colors v0.0.0-20260710212606-b68ba398853d - unikraft.com/x/filters v0.0.0-20260416164455-ec39ae908f3f + unikraft.com/x/filters v0.0.0-20260804153219-d1b47a40e047 unikraft.com/x/fingerprint v0.0.0-20260126094137-ab6e717e5679 unikraft.com/x/guesstermwidth v0.0.0-20260708141520-0cbb0a92056b unikraft.com/x/iata v0.0.0-20260713183529-fd34645687a0 @@ -194,5 +194,3 @@ require ( ) tool github.com/caarlos0/svu/v3 - -replace unikraft.com/x/filters => ../x/filters diff --git a/go.sum b/go.sum index 3642b1d9..ea886459 100644 --- a/go.sum +++ b/go.sum @@ -488,6 +488,8 @@ unikraft.com/cloud/sdk v0.3.1-0.20260730110715-92b5bae5afbe h1:oupb6urtot/mVqphO unikraft.com/cloud/sdk v0.3.1-0.20260730110715-92b5bae5afbe/go.mod h1:m/TX4opCAcR4OFFP/X8TWAjtzzW0Z6FxuVYznw9UW2Q= unikraft.com/x/colors v0.0.0-20260710212606-b68ba398853d h1:xNtt0WCZRm/WIr3F48AnYc7m41W6TqgvsqprDbXi4/A= unikraft.com/x/colors v0.0.0-20260710212606-b68ba398853d/go.mod h1:4jHfTebx2ff1Gd5MbS/xMrGFKRQTuwvTdzmAzaJWlSM= +unikraft.com/x/filters v0.0.0-20260804153219-d1b47a40e047 h1:ZeMtVwVhGtiEIwnHXbzp4NAtiZp2pCAhEro0qPN+Vgs= +unikraft.com/x/filters v0.0.0-20260804153219-d1b47a40e047/go.mod h1:deba9Bu2u3ibYXGbsO1QRxFPuXHQXoiuY6eFmwTbuLU= unikraft.com/x/fingerprint v0.0.0-20260126094137-ab6e717e5679 h1:zdvJjNkjsriS8RM46FcdgcRoCh4EYM66PGjqVgi/ups= unikraft.com/x/fingerprint v0.0.0-20260126094137-ab6e717e5679/go.mod h1:FP7uOxux/W5PKqSRQsR4tyjNuLq4Cfio7mc5QVH1kW8= unikraft.com/x/guesstermwidth v0.0.0-20260708141520-0cbb0a92056b h1:/VC/nXdu6k0G+a9Lkd9P/sz1r1mrlh0rocNtr1M84V8= diff --git a/internal/resource/cmd/cmd_test.go b/internal/resource/cmd/cmd_test.go index 22fcac20..b2e51004 100644 --- a/internal/resource/cmd/cmd_test.go +++ b/internal/resource/cmd/cmd_test.go @@ -45,6 +45,7 @@ func setupTestEnv() *resourcet.TestEnv { Foo: 42, Bar: "hello", Score: 10.0, + Flag: true, }, Authors: []resourcet.TestAuthor{ {Name: "Alice", Email: "alice@example.com"}, @@ -2148,6 +2149,42 @@ func TestFilterComparisonOperators(t *testing.T) { assert.Empty(t, strings.TrimSpace(output)) }) + t.Run("string_field_ordering_disallowed_even_with_differing_values", func(t *testing.T) { + for _, filter := range []string{ + "settings.bar>hello", + "settings.bar>=hello", + "settings.barfalse", + "settings.flag>=false", + "settings.flag7") require.NoError(t, err) diff --git a/internal/resource/cmd/filter.go b/internal/resource/cmd/filter.go index dbc0880b..732cf60c 100644 --- a/internal/resource/cmd/filter.go +++ b/internal/resource/cmd/filter.go @@ -107,16 +107,13 @@ func (a *fieldAdaptor) Entries() []string { return a.entries } -func (a *fieldAdaptor) Compare(other string) (int, bool) { +func (a *fieldAdaptor) compareValue(other string) (int, bool) { if a.sliceVal != nil { return 0, false } if a.field == nil || a.field.Value == nil { return 0, false } - if !isOrderable(a.field.Value) { - return 0, false - } parsed, err := value.ParseNew([]string{other}, a.field.Value) if err != nil { return 0, false @@ -124,7 +121,22 @@ func (a *fieldAdaptor) Compare(other string) (int, bool) { return value.Compare(a.field.Value, parsed), true } +func (a *fieldAdaptor) Equals(other string) (bool, bool) { + result, ok := a.compareValue(other) + return result == 0, ok +} + +func (a *fieldAdaptor) Compare(other string) (int, bool) { + if a.sliceVal != nil || a.field == nil || !isOrderable(a.field.Value) { + return 0, false + } + return a.compareValue(other) +} + func isOrderable(v any) bool { + if v == nil { + return false + } rv := reflect.ValueOf(v) for rv.Kind() == reflect.Pointer { if rv.IsNil() { @@ -135,8 +147,7 @@ func isOrderable(v any) bool { switch rv.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, - reflect.Float32, reflect.Float64, - reflect.Bool, reflect.String: + reflect.Float32, reflect.Float64: return true } return rv.Type().ConvertibleTo(reflect.TypeFor[time.Time]()) diff --git a/internal/resource/testing/resource.go b/internal/resource/testing/resource.go index b0e68395..e67cab6f 100644 --- a/internal/resource/testing/resource.go +++ b/internal/resource/testing/resource.go @@ -79,6 +79,7 @@ type TestSettings struct { Foo int Bar string Score float64 + Flag bool } type TestAuthor struct { @@ -216,6 +217,11 @@ func (t TestResource) Fields(ctx context.Context) ([]resource.Field, error) { Value: t.Settings.Score, Verbosity: resource.FieldVerbosityLong, }, + { + Name: "flag", + Value: t.Settings.Flag, + Verbosity: resource.FieldVerbosityLong, + }, }, }, { From 9fe4efffca03bbc35b243596913fe35e4cdad02f Mon Sep 17 00:00:00 2001 From: Justin Chadwell Date: Tue, 4 Aug 2026 16:37:32 +0100 Subject: [PATCH 3/3] fix(filters): Allow wildcard matching on scalar slice fields Signed-off-by: Justin Chadwell --- internal/resource/cmd/cmd_test.go | 23 +++++++++++++++++++++++ internal/resource/cmd/filter.go | 21 ++++++--------------- internal/resource/testing/resource.go | 6 ++++++ 3 files changed, 35 insertions(+), 15 deletions(-) diff --git a/internal/resource/cmd/cmd_test.go b/internal/resource/cmd/cmd_test.go index b2e51004..607831b1 100644 --- a/internal/resource/cmd/cmd_test.go +++ b/internal/resource/cmd/cmd_test.go @@ -51,6 +51,7 @@ func setupTestEnv() *resourcet.TestEnv { {Name: "Alice", Email: "alice@example.com"}, {Name: "Bob", Email: "bob@example.com"}, }, + Tags: []string{"prod", "web"}, }) env.Add(resourcet.TestResource{ ID: "id-test2", @@ -68,6 +69,7 @@ func setupTestEnv() *resourcet.TestEnv { {Name: "Charlie", Email: "charlie@example.com"}, {Name: "Dana", Email: "dana@example.com"}, }, + Tags: []string{"staging"}, }) return env } @@ -2167,6 +2169,27 @@ func TestFilterComparisonOperators(t *testing.T) { assert.NotContains(t, output, "test2") }) + t.Run("scalar_slice_indexed_equal", func(t *testing.T) { + output, err := runFilter(t, "tags.0==prod") + require.NoError(t, err) + assert.Contains(t, output, "test1") + assert.NotContains(t, output, "test2") + }) + + t.Run("scalar_slice_wildcard_equal", func(t *testing.T) { + output, err := runFilter(t, "tags.*==staging") + require.NoError(t, err) + assert.Contains(t, output, "test2") + assert.NotContains(t, output, "test1") + }) + + t.Run("scalar_slice_wildcard_not_equal", func(t *testing.T) { + output, err := runFilter(t, "tags.*!=prod") + require.NoError(t, err) + assert.Contains(t, output, "test2") + assert.NotContains(t, output, "test1") + }) + t.Run("bool_field_ordering_disallowed", func(t *testing.T) { for _, filter := range []string{ "settings.flag>false", diff --git a/internal/resource/cmd/filter.go b/internal/resource/cmd/filter.go index 732cf60c..58d1d197 100644 --- a/internal/resource/cmd/filter.go +++ b/internal/resource/cmd/filter.go @@ -22,7 +22,6 @@ type fieldAdaptor struct { children []resource.Field field *resource.Field entries []string - sliceVal *string } func newFieldAdaptor(fields []resource.Field) filters.Adaptor { @@ -30,9 +29,10 @@ func newFieldAdaptor(fields []resource.Field) filters.Adaptor { } func (a *fieldAdaptor) Select(key []string) (filters.Adaptor, bool) { - if a.sliceVal != nil { - return nil, false + if len(key) == 0 && a.field != nil { + return a, true } + matched := resource.GetFieldByPath(a.children, key) if len(matched) == 0 { if len(key) >= 1 { @@ -47,8 +47,8 @@ func (a *fieldAdaptor) Select(key []string) (filters.Adaptor, bool) { if slice, sok := getSliceValue(parent.Value); sok { idx, err := strconv.Atoi(key[len(key)-1]) if err == nil && idx >= 0 && idx < len(slice) { - s := slice[idx] - return &fieldAdaptor{sliceVal: &s}, true + f := resource.Field{Name: key[len(key)-1], Value: slice[idx]} + return &fieldAdaptor{field: &f}, true } } } @@ -83,9 +83,6 @@ func (a *fieldAdaptor) Select(key []string) (filters.Adaptor, bool) { } func (a *fieldAdaptor) String() string { - if a.sliceVal != nil { - return *a.sliceVal - } if a.field == nil { return "" } @@ -94,9 +91,6 @@ func (a *fieldAdaptor) String() string { } func (a *fieldAdaptor) Value() any { - if a.sliceVal != nil { - return *a.sliceVal - } if a.field == nil { return nil } @@ -108,9 +102,6 @@ func (a *fieldAdaptor) Entries() []string { } func (a *fieldAdaptor) compareValue(other string) (int, bool) { - if a.sliceVal != nil { - return 0, false - } if a.field == nil || a.field.Value == nil { return 0, false } @@ -127,7 +118,7 @@ func (a *fieldAdaptor) Equals(other string) (bool, bool) { } func (a *fieldAdaptor) Compare(other string) (int, bool) { - if a.sliceVal != nil || a.field == nil || !isOrderable(a.field.Value) { + if a.field == nil || !isOrderable(a.field.Value) { return 0, false } return a.compareValue(other) diff --git a/internal/resource/testing/resource.go b/internal/resource/testing/resource.go index e67cab6f..37eae2ca 100644 --- a/internal/resource/testing/resource.go +++ b/internal/resource/testing/resource.go @@ -65,6 +65,7 @@ type TestResource struct { Lazy string // Computed via callback when requested Settings TestSettings Authors []TestAuthor + Tags []string } var ( @@ -262,6 +263,11 @@ func (t TestResource) Fields(ctx context.Context) ([]resource.Field, error) { return fields }(), }, + { + Name: "tags", + Value: t.Tags, + Verbosity: resource.FieldVerbosityLong, + }, }, nil }