From cc2e7052201698a89fbf6d1ca04cc8b26863adc2 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 8 Mar 2026 23:29:27 +0000 Subject: [PATCH] refactor(selectors): share exact-match resolution helpers --- internal/cmd/name_resolution.go | 36 ++++----------- internal/cmd/selector_match.go | 62 +++++++++++++++++++++++++ internal/cmd/sheets_range_resolve.go | 68 +++++++--------------------- 3 files changed, 89 insertions(+), 77 deletions(-) create mode 100644 internal/cmd/selector_match.go diff --git a/internal/cmd/name_resolution.go b/internal/cmd/name_resolution.go index dafda06..87000c2 100644 --- a/internal/cmd/name_resolution.go +++ b/internal/cmd/name_resolution.go @@ -66,12 +66,7 @@ func resolveTasklistID(ctx context.Context, svc *tasks.Service, input string) (s return in, nil } - type match struct { - ID string - Title string - } - - var titleMatches []match + var options []selectorMatch seenTokens := map[string]bool{} pageToken := "" for { @@ -92,13 +87,10 @@ func resolveTasklistID(ctx context.Context, svc *tasks.Service, input string) (s if tl == nil { continue } - id := strings.TrimSpace(tl.Id) - if id != "" && id == in { - return in, nil - } - if id != "" && strings.EqualFold(strings.TrimSpace(tl.Title), in) { - titleMatches = append(titleMatches, match{ID: id, Title: strings.TrimSpace(tl.Title)}) - } + options = append(options, selectorMatch{ + ID: strings.TrimSpace(tl.Id), + Name: strings.TrimSpace(tl.Title), + }) } next := strings.TrimSpace(resp.NextPageToken) if next == "" { @@ -107,20 +99,12 @@ func resolveTasklistID(ctx context.Context, svc *tasks.Service, input string) (s pageToken = next } - if len(titleMatches) == 1 { - return titleMatches[0].ID, nil + match, found, err := findByIDOrCaseFoldName(in, "tasklist", options) + if err != nil { + return "", err } - if len(titleMatches) > 1 { - sort.Slice(titleMatches, func(i, j int) bool { return titleMatches[i].ID < titleMatches[j].ID }) - parts := make([]string, 0, len(titleMatches)) - for _, m := range titleMatches { - label := m.Title - if label == "" { - label = "(untitled)" - } - parts = append(parts, fmt.Sprintf("%s (%s)", label, m.ID)) - } - return "", usagef("ambiguous tasklist %q; matches: %s", in, strings.Join(parts, ", ")) + if found { + return match.ID, nil } return in, nil diff --git a/internal/cmd/selector_match.go b/internal/cmd/selector_match.go new file mode 100644 index 0000000..15bd1f8 --- /dev/null +++ b/internal/cmd/selector_match.go @@ -0,0 +1,62 @@ +package cmd + +import ( + "fmt" + "sort" + "strings" +) + +type selectorMatch struct { + ID string + Name string +} + +func findByIDOrCaseFoldName(input, kind string, options []selectorMatch) (*selectorMatch, bool, error) { + in := strings.TrimSpace(input) + if in == "" { + return nil, false, nil + } + + for _, option := range options { + if strings.TrimSpace(option.ID) == in { + match := option + return &match, true, nil + } + } + + var matches []selectorMatch + for _, option := range options { + name := strings.TrimSpace(option.Name) + if name == "" || !strings.EqualFold(name, in) { + continue + } + matches = append(matches, selectorMatch{ + ID: strings.TrimSpace(option.ID), + Name: name, + }) + } + + switch len(matches) { + case 0: + return nil, false, nil + case 1: + match := matches[0] + return &match, true, nil + default: + sort.Slice(matches, func(i, j int) bool { + if matches[i].Name == matches[j].Name { + return matches[i].ID < matches[j].ID + } + return matches[i].Name < matches[j].Name + }) + parts := make([]string, 0, len(matches)) + for _, match := range matches { + label := match.Name + if label == "" { + label = "(unnamed)" + } + parts = append(parts, fmt.Sprintf("%s (%s)", label, match.ID)) + } + return nil, false, usagef("ambiguous %s %q; matches: %s", kind, in, strings.Join(parts, ", ")) + } +} diff --git a/internal/cmd/sheets_range_resolve.go b/internal/cmd/sheets_range_resolve.go index 55f92d4..0232cbf 100644 --- a/internal/cmd/sheets_range_resolve.go +++ b/internal/cmd/sheets_range_resolve.go @@ -3,7 +3,6 @@ package cmd import ( "context" "fmt" - "sort" "strings" "google.golang.org/api/sheets/v4" @@ -107,11 +106,6 @@ func resolveGridRangeWithCatalog(input string, catalog *spreadsheetRangeCatalog, return nil, usagef("unknown named range %q", in) } -type namedRangeMatch struct { - ID string - Name string -} - // resolveNamedRangeByNameOrID finds a named range by: // - exact ID match, or // - case-insensitive exact name match (errors if ambiguous). @@ -123,56 +117,28 @@ func resolveNamedRangeByNameOrID(input string, namedRanges []*sheets.NamedRange) return nil, false, nil } - // Prefer an exact ID match without any ambiguity. + options := make([]selectorMatch, 0, len(namedRanges)) for _, nr := range namedRanges { if nr == nil { continue } - if strings.TrimSpace(nr.NamedRangeId) == in { + options = append(options, selectorMatch{ + ID: strings.TrimSpace(nr.NamedRangeId), + Name: strings.TrimSpace(nr.Name), + }) + } + + match, found, err := findByIDOrCaseFoldName(in, "named range", options) + if err != nil { + return nil, false, err + } + if !found { + return nil, false, nil + } + for _, nr := range namedRanges { + if nr != nil && strings.TrimSpace(nr.NamedRangeId) == match.ID { return nr, true, nil } } - - var matches []namedRangeMatch - for _, nr := range namedRanges { - if nr == nil { - continue - } - name := strings.TrimSpace(nr.Name) - if name == "" { - continue - } - if strings.EqualFold(name, in) { - matches = append(matches, namedRangeMatch{ID: strings.TrimSpace(nr.NamedRangeId), Name: name}) - } - } - - if len(matches) == 0 { - return nil, false, nil - } - if len(matches) == 1 { - for _, nr := range namedRanges { - if nr != nil && strings.TrimSpace(nr.NamedRangeId) == matches[0].ID { - return nr, true, nil - } - } - // Shouldn't happen, but be safe. - return nil, false, fmt.Errorf("named range match disappeared (id=%q)", matches[0].ID) - } - - sort.Slice(matches, func(i, j int) bool { - if matches[i].Name == matches[j].Name { - return matches[i].ID < matches[j].ID - } - return matches[i].Name < matches[j].Name - }) - parts := make([]string, 0, len(matches)) - for _, m := range matches { - label := m.Name - if label == "" { - label = "(unnamed)" - } - parts = append(parts, fmt.Sprintf("%s (%s)", label, m.ID)) - } - return nil, false, usagef("ambiguous named range %q; matches: %s", in, strings.Join(parts, ", ")) + return nil, false, fmt.Errorf("named range match disappeared (id=%q)", match.ID) }