Skip to content

Commit

Permalink
feat: implement enum completion
Browse files Browse the repository at this point in the history
  • Loading branch information
odsod committed Jul 29, 2022
1 parent 60c452e commit bf8ce0f
Show file tree
Hide file tree
Showing 7 changed files with 272 additions and 175 deletions.
52 changes: 11 additions & 41 deletions aipcli/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import (
"strings"

"github.com/spf13/cobra"
"go.einride.tech/aip/resourcename"
"go.einride.tech/aip-cli/internal/protoshell"
"google.golang.org/protobuf/reflect/protoreflect"
)

type CompletionFunc func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective)
Expand All @@ -31,6 +32,13 @@ func fieldCompletionFunc(comment string) CompletionFunc {
}
}

func enumFieldCompletionFunc(comment string, values protoreflect.EnumValueDescriptors) CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return cobra.AppendActiveHelp(protoshell.CompleteEnumValue(toComplete, values), trimFieldComment(comment)),
cobra.ShellCompDirectiveNoFileComp
}
}

func timestampCompletionFunc(comment string) CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
activeHelp := trimFieldComment(comment)
Expand All @@ -44,7 +52,7 @@ func resourceNameCompletionFunc(comment string, patterns ...string) CompletionFu
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
result := make([]string, 0, len(patterns))
for _, pattern := range patterns {
if completion, ok := completeResourceName(toComplete, pattern); ok {
if completion, ok := protoshell.CompleteResourceName(toComplete, pattern); ok {
result = append(result, fmt.Sprintf("%s\t%s", completion, pattern))
}
}
Expand All @@ -59,7 +67,7 @@ func resourceNameListCompletionFunc(comment string, patterns ...string) Completi
lastToCompleteElement := toCompleteElements[len(toCompleteElements)-1]
result := make([]string, 0, len(patterns))
for _, pattern := range patterns {
if elementCompletion, ok := completeResourceName(lastToCompleteElement, pattern); ok {
if elementCompletion, ok := protoshell.CompleteResourceName(lastToCompleteElement, pattern); ok {
var completion string
if len(toCompleteElements) > 1 {
completion = strings.Join(
Expand All @@ -76,41 +84,3 @@ func resourceNameListCompletionFunc(comment string, patterns ...string) Completi
return result, cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp
}
}

func completeResourceName(toComplete, pattern string) (string, bool) {
toCompleteSegments := strings.Split(toComplete, "/")
patternSegments := strings.Split(pattern, "/")
if len(toCompleteSegments) > len(patternSegments) {
return "", false
}
var result strings.Builder
result.Grow(len(pattern))
for i, toCompleteSegment := range toCompleteSegments {
patternSegment := patternSegments[i]
if resourcename.Segment(patternSegment).IsVariable() {
result.WriteString(toCompleteSegment)
if i < len(toCompleteSegments)-1 {
result.WriteByte('/')
}
continue
}
if toCompleteSegment == patternSegment {
result.WriteString(patternSegment)
if i < len(toCompleteSegments)-1 || i < len(patternSegments)-1 {
result.WriteByte('/')
}
continue
}
if i < len(toCompleteSegments)-1 {
return "", false
}
if !strings.HasPrefix(patternSegment, toCompleteSegment) {
return "", false
}
result.WriteString(patternSegment)
if i < len(patternSegments)-1 {
result.WriteByte('/')
}
}
return result.String(), result.String() != ""
}
134 changes: 0 additions & 134 deletions aipcli/completion_test.go
Original file line number Diff line number Diff line change
@@ -1,135 +1 @@
package aipcli

import (
"testing"

"gotest.tools/v3/assert"
)

func TestCompleteResourceName(t *testing.T) {
for _, tt := range []struct {
name string
toComplete string
pattern string
completion string
ok bool
}{
{
name: "mismatch",
toComplete: "user",
pattern: "shippers/{shipper}/shipments/{shipment}",
completion: "",
ok: false,
},

{
name: "empty input",
toComplete: "",
pattern: "shippers/{shipper}/shipments/{shipment}",
completion: "shippers/",
ok: true,
},

{
name: "partial segment with singleton pattern",
toComplete: "ship",
pattern: "shippers",
completion: "shippers",
ok: true,
},

{
name: "empty input with singleton pattern",
toComplete: "",
pattern: "shippers",
completion: "shippers",
ok: true,
},

{
name: "empty input with variable pattern",
toComplete: "",
pattern: "{shipper}",
completion: "",
ok: false,
},

{
name: "resource ID input with variable pattern",
toComplete: "foo",
pattern: "{shipper}",
completion: "foo",
ok: true,
},

{
name: "partial segment",
toComplete: "shi",
pattern: "shippers/{shipper}/shipments/{shipment}",
completion: "shippers/",
ok: true,
},

{
name: "full segment",
toComplete: "shippers",
pattern: "shippers/{shipper}/shipments/{shipment}",
completion: "shippers/",
ok: true,
},

{
name: "full segment with slash",
toComplete: "shippers/",
pattern: "shippers/{shipper}/shipments/{shipment}",
completion: "shippers/",
ok: true,
},

{
name: "partial resource ID",
toComplete: "shippers/test",
pattern: "shippers/{shipper}/shipments/{shipment}",
completion: "shippers/test",
ok: true,
},

{
name: "full resource ID",
toComplete: "shippers/test/",
pattern: "shippers/{shipper}/shipments/{shipment}",
completion: "shippers/test/shipments/",
ok: true,
},

{
name: "partial second collection",
toComplete: "shippers/foo/ship",
pattern: "shippers/{shipper}/shipments/{shipment}",
completion: "shippers/foo/shipments/",
ok: true,
},

{
name: "second collection mismatch",
toComplete: "shippers/foo/sit",
pattern: "shippers/{shipper}/shipments/{shipment}",
completion: "",
ok: false,
},

{
name: "partial second resource ID",
toComplete: "shippers/foo/shipments/bar",
pattern: "shippers/{shipper}/shipments/{shipment}",
completion: "shippers/foo/shipments/bar",
ok: true,
},
} {
t.Run(tt.name, func(t *testing.T) {
actual, ok := completeResourceName(tt.toComplete, tt.pattern)
assert.Equal(t, tt.ok, ok)
assert.Equal(t, tt.completion, actual)
})
}
}
4 changes: 4 additions & 0 deletions aipcli/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,10 @@ func registerCompletion(
_ = cmd.RegisterFlagCompletionFunc(flag.Name, timestampCompletionFunc(comment))
return
}
if field.Kind() == protoreflect.EnumKind && !field.IsList() {
_ = cmd.RegisterFlagCompletionFunc(flag.Name, enumFieldCompletionFunc(comment, field.Enum().Values()))
return
}
// default: register active help with field comment
_ = cmd.RegisterFlagCompletionFunc(flag.Name, fieldCompletionFunc(comment))
}
Expand Down
22 changes: 22 additions & 0 deletions internal/protoshell/enum.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package protoshell

import (
"strings"

"google.golang.org/protobuf/reflect/protoreflect"
)

// CompleteEnumValue returns valid values to complete for the provided enum values.
func CompleteEnumValue(toComplete string, values protoreflect.EnumValueDescriptors) []string {
result := make([]string, 0, values.Len())
for i := 0; i < values.Len(); i++ {
value := string(values.Get(i).Name())
if strings.HasPrefix(value, toComplete) {
result = append(result, value)
}
}
if len(result) == 0 {
return nil
}
return result
}
54 changes: 54 additions & 0 deletions internal/protoshell/enum_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package protoshell

import (
"testing"

ltype "google.golang.org/genproto/googleapis/logging/type"
"google.golang.org/protobuf/reflect/protoreflect"
"gotest.tools/v3/assert"
)

func TestCompleteEnum(t *testing.T) {
for _, tt := range []struct {
name string
toComplete string
enum protoreflect.EnumValueDescriptors
expected []string
}{
{
name: "empty",
toComplete: "",
enum: ltype.LogSeverity_DEFAULT.Descriptor().Values(),
expected: []string{
"DEFAULT", "DEBUG", "INFO", "NOTICE", "WARNING", "ERROR", "CRITICAL", "ALERT", "EMERGENCY",
},
},

{
name: "prefix match",
toComplete: "E",
enum: ltype.LogSeverity_DEFAULT.Descriptor().Values(),
expected: []string{"ERROR", "EMERGENCY"},
},

{
name: "no match",
toComplete: "Z",
enum: ltype.LogSeverity_DEFAULT.Descriptor().Values(),
expected: nil,
},

{
name: "exact match",
toComplete: "DEFAULT",
enum: ltype.LogSeverity_DEFAULT.Descriptor().Values(),
expected: []string{"DEFAULT"},
},
} {
tt := tt
t.Run(tt.name, func(t *testing.T) {
actual := CompleteEnumValue(tt.toComplete, tt.enum)
assert.DeepEqual(t, tt.expected, actual)
})
}
}
46 changes: 46 additions & 0 deletions internal/protoshell/resourcename.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package protoshell

import (
"strings"

"go.einride.tech/aip/resourcename"
)

// CompleteResourceName returns a value to complete for the provided resource name pattern.
func CompleteResourceName(toComplete, pattern string) (string, bool) {
toCompleteSegments := strings.Split(toComplete, "/")
patternSegments := strings.Split(pattern, "/")
if len(toCompleteSegments) > len(patternSegments) {
return "", false
}
var result strings.Builder
result.Grow(len(pattern))
for i, toCompleteSegment := range toCompleteSegments {
patternSegment := patternSegments[i]
if resourcename.Segment(patternSegment).IsVariable() {
result.WriteString(toCompleteSegment)
if i < len(toCompleteSegments)-1 {
result.WriteByte('/')
}
continue
}
if toCompleteSegment == patternSegment {
result.WriteString(patternSegment)
if i < len(toCompleteSegments)-1 || i < len(patternSegments)-1 {
result.WriteByte('/')
}
continue
}
if i < len(toCompleteSegments)-1 {
return "", false
}
if !strings.HasPrefix(patternSegment, toCompleteSegment) {
return "", false
}
result.WriteString(patternSegment)
if i < len(patternSegments)-1 {
result.WriteByte('/')
}
}
return result.String(), result.String() != ""
}
Loading

0 comments on commit bf8ce0f

Please sign in to comment.