Skip to content

Commit 27ed056

Browse files
authored
feat: allow using UUID prefix as argument (#341)
Also refactors argument resolution to be more flexible. This allows later adding support for case-insensitive and wild-card matching.
1 parent 655bbda commit 27ed056

31 files changed

Lines changed: 332 additions & 299 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Changed
1111

1212
- Take server state into account in server completions. For example, do not offer started servers as completions for `server start` command.
13+
- Allow using UUID prefix as an argument. For example, if there is only one network available that has an UUID starting with `0316`, details of that network can be listed with `upctl network show 0316` command.
1314

1415
## [3.11.1] - 2024-08-12
1516

internal/commands/network/modify.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@ func (s *modifyCommand) ExecuteSingleArgument(exec commands.Executor, arg string
9696
if err != nil {
9797
return commands.HandleError(exec, msg, fmt.Errorf("cannot get router resolver: %w", err))
9898
}
99-
routerUUID, err := routerResolver(s.attachRouter)
99+
resolved := routerResolver(s.attachRouter)
100+
routerUUID, err := resolved.GetOnly()
100101
if err != nil {
101102
return commands.HandleError(exec, msg, fmt.Errorf("cannot resolve router '%s': %w", s.attachRouter, err))
102103
}

internal/commands/runcommand.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,9 @@ func resolveArguments(nc Command, exec Executor, args []string) (out []resolvedA
7878
return nil, fmt.Errorf("cannot get resolver: %w", err)
7979
}
8080
for _, arg := range args {
81-
resolved, err := argumentResolver(arg)
82-
out = append(out, resolvedArgument{Resolved: resolved, Error: err, Original: arg})
81+
resolved := argumentResolver(arg)
82+
value, err := resolved.GetOnly()
83+
out = append(out, resolvedArgument{Resolved: value, Error: err, Original: arg})
8384
}
8485
} else {
8586
for _, arg := range args {

internal/commands/runcommand_test.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,12 @@ type mockMultiResolver struct {
9999
}
100100

101101
func (m *mockMultiResolver) Get(_ context.Context, _ internal.AllServices) (resolver.Resolver, error) {
102-
return func(arg string) (uuid string, err error) {
103-
if len(arg) > 5 {
104-
return "", fmt.Errorf("MOCKTOOLONG")
102+
return func(arg string) resolver.Resolved {
103+
rv := resolver.Resolved{Arg: arg}
104+
if len(arg) <= 5 {
105+
rv.AddMatch("uuid:"+arg, resolver.MatchTypeExact)
105106
}
106-
return fmt.Sprintf("uuid:%s", arg), nil
107+
return rv
107108
}, nil
108109
}
109110

@@ -258,7 +259,7 @@ func TestExecute_Resolution(t *testing.T) {
258259
values[typedO.Value.(string)] = struct{}{}
259260
case output.Error:
260261
assert.Empty(t, typedO.Resolved)
261-
assert.EqualError(t, typedO.Value, "cannot resolve argument: MOCKTOOLONG")
262+
assert.EqualError(t, typedO.Value, "cannot resolve argument: nothing found matching 'failtoresolve'")
262263
}
263264
}
264265
assert.Equal(t, values, map[string]struct{}{

internal/namedargs/resolve.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@ func Resolve(provider resolver.ResolutionProvider, exec commands.Executor, arg s
1414
return "", fmt.Errorf("could not initialize resolver: %w", err)
1515
}
1616

17-
return resolver(arg)
17+
resolved := resolver(arg)
18+
return resolved.GetOnly()
1819
}

internal/resolver/account.go

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,12 @@ func (s CachingAccount) Get(ctx context.Context, svc internal.AllServices) (Reso
1818
if err != nil {
1919
return nil, err
2020
}
21-
return func(arg string) (uuid string, err error) {
22-
rv := ""
21+
return func(arg string) Resolved {
22+
rv := Resolved{Arg: arg}
2323
for _, account := range accounts {
24-
if MatchArgWithWhitespace(arg, account.Username) {
25-
if rv != "" {
26-
return "", AmbiguousResolutionError(arg)
27-
}
28-
rv = account.Username
29-
}
24+
rv.AddMatch(account.Username, MatchArgWithWhitespace(arg, account.Username))
3025
}
31-
if rv != "" {
32-
return rv, nil
33-
}
34-
return "", NotFoundError(arg)
26+
return rv
3527
}, nil
3628
}
3729

internal/resolver/account_test.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,10 @@ func TestAccountResolution(t *testing.T) {
4343
argResolver, err := res.Get(context.TODO(), mService)
4444
assert.NoError(t, err)
4545
for _, account := range allAccounts {
46-
resolved, err := argResolver(account.Username)
46+
resolved := argResolver(account.Username)
47+
value, err := resolved.GetOnly()
4748
assert.NoError(t, err)
48-
assert.Equal(t, account.Username, resolved)
49+
assert.Equal(t, account.Username, value)
4950
}
5051
// make sure caching works, eg. we didn't call GetAccountList more than once
5152
mService.AssertNumberOfCalls(t, "GetAccountList", 1)
@@ -60,12 +61,14 @@ func TestAccountResolution(t *testing.T) {
6061
assert.NoError(t, err)
6162

6263
// not found
63-
resolved, err := argResolver("notfound")
64+
resolved := argResolver("notfound")
65+
value, err := resolved.GetOnly()
66+
6467
if !assert.Error(t, err) {
6568
t.FailNow()
6669
}
6770
assert.ErrorIs(t, err, resolver.NotFoundError("notfound"))
68-
assert.Equal(t, "", resolved)
71+
assert.Equal(t, "", value)
6972

7073
// make sure caching works, eg. we didn't call GetAccountList more than once
7174
mService.AssertNumberOfCalls(t, "GetAccountList", 1)

internal/resolver/database.go

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,13 @@ func (s CachingDatabase) Get(ctx context.Context, svc internal.AllServices) (Res
1919
if err != nil {
2020
return nil, err
2121
}
22-
return func(arg string) (uuid string, err error) {
23-
rv := ""
22+
return func(arg string) Resolved {
23+
rv := Resolved{Arg: arg}
2424
for _, db := range databases {
25-
if MatchArgWithWhitespace(arg, db.Title) || db.UUID == arg {
26-
if rv != "" {
27-
return "", AmbiguousResolutionError(arg)
28-
}
29-
rv = db.UUID
30-
}
25+
rv.AddMatch(db.UUID, MatchArgWithWhitespace(arg, db.Title))
26+
rv.AddMatch(db.UUID, MatchUUID(arg, db.UUID))
3127
}
32-
if rv != "" {
33-
return rv, nil
34-
}
35-
return "", NotFoundError(arg)
28+
return rv
3629
}, nil
3730
}
3831

internal/resolver/database_test.go

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,10 @@ func TestDatabaseResolution(t *testing.T) {
2727
argResolver, err := res.Get(context.TODO(), mService)
2828
assert.NoError(t, err)
2929
for _, db := range mockDatabases {
30-
resolved, err := argResolver(db.UUID)
30+
resolved := argResolver(db.UUID)
31+
value, err := resolved.GetOnly()
3132
assert.NoError(t, err)
32-
assert.Equal(t, db.UUID, resolved)
33+
assert.Equal(t, db.UUID, value)
3334
}
3435

3536
// Make sure caching works, eg. we didn't call GetManagedDatabases more than once
@@ -44,9 +45,10 @@ func TestDatabaseResolution(t *testing.T) {
4445
assert.NoError(t, err)
4546

4647
db := mockDatabases[2]
47-
resolved, err := argResolver(db.Title)
48+
resolved := argResolver(db.Title)
49+
value, err := resolved.GetOnly()
4850
assert.NoError(t, err)
49-
assert.Equal(t, db.UUID, resolved)
51+
assert.Equal(t, db.UUID, value)
5052
// Make sure caching works, eg. we didn't call GetManagedDatabases more than once
5153
mService.AssertNumberOfCalls(t, "GetManagedDatabases", 1)
5254
})
@@ -58,23 +60,25 @@ func TestDatabaseResolution(t *testing.T) {
5860
res := resolver.CachingDatabase{}
5961
argResolver, err := res.Get(context.TODO(), mService)
6062
assert.NoError(t, err)
61-
var resolved string
6263

6364
// Ambiguous title
64-
resolved, err = argResolver("asd")
65+
resolved := argResolver("asd")
66+
value, err := resolved.GetOnly()
6567
if !assert.Error(t, err) {
6668
t.FailNow()
6769
}
6870
assert.ErrorIs(t, err, resolver.AmbiguousResolutionError("asd"))
69-
assert.Equal(t, "", resolved)
71+
assert.Equal(t, "", value)
7072

7173
// Not found
72-
resolved, err = argResolver("not-found")
74+
resolved = argResolver("not-found")
75+
value, err = resolved.GetOnly()
76+
7377
if !assert.Error(t, err) {
7478
t.FailNow()
7579
}
7680
assert.ErrorIs(t, err, resolver.NotFoundError("not-found"))
77-
assert.Equal(t, "", resolved)
81+
assert.Equal(t, "", value)
7882

7983
// Make sure caching works, eg. we didn't call GetManagedDatabases more than once
8084
mService.AssertNumberOfCalls(t, "GetManagedDatabases", 1)

internal/resolver/gateway.go

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,13 @@ func (s CachingGateway) Get(ctx context.Context, svc internal.AllServices) (Reso
1818
if err != nil {
1919
return nil, err
2020
}
21-
return func(arg string) (uuid string, err error) {
22-
rv := ""
21+
return func(arg string) Resolved {
22+
rv := Resolved{Arg: arg}
2323
for _, gtw := range gateways {
24-
if MatchArgWithWhitespace(arg, gtw.Name) || gtw.UUID == arg {
25-
if rv != "" {
26-
return "", AmbiguousResolutionError(arg)
27-
}
28-
rv = gtw.UUID
29-
}
24+
rv.AddMatch(gtw.UUID, MatchArgWithWhitespace(arg, gtw.Name))
25+
rv.AddMatch(gtw.UUID, MatchUUID(arg, gtw.UUID))
3026
}
31-
if rv != "" {
32-
return rv, nil
33-
}
34-
return "", NotFoundError(arg)
27+
return rv
3528
}, nil
3629
}
3730

0 commit comments

Comments
 (0)