refactor(selectors): share exact-match resolution helpers
This commit is contained in:
parent
644951e87b
commit
cc2e705220
@ -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
|
||||
|
||||
62
internal/cmd/selector_match.go
Normal file
62
internal/cmd/selector_match.go
Normal file
@ -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, ", "))
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user