Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,11 @@ func (o *ServerOptions) Attach(c *cobra.Command) error {

`Complete<FieldName>` works for any field that becomes a flag (not only `flagcustom:"true"` fields).

Completion precedence:

- If a completion function is already registered on a flag before `structcli.Define()`, structcli preserves it.
- If `structcli.Define()` auto-registers `Complete<FieldName>`, a later manual `RegisterFlagCompletionFunc` on the same flag returns Cobra's `already registered` error.

In [values](/values/values.go) we provide `pflag.Value` implementations for standard types.

See [full example](examples/full/cli/cli.go) for more details.
Expand Down
13 changes: 13 additions & 0 deletions define_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,19 @@ func (suite *structcliSuite) TestDefine_DoesNotOverrideExistingFlagCompletion()
assert.Equal(suite.T(), cobra.ShellCompDirectiveDefault, directive)
}

func (suite *structcliSuite) TestDefine_ManualCompletionRegistrationAfterAutoRegistrationFails() {
opts := &autoCompleteOptions{}
c := &cobra.Command{Use: "test"}

require.NoError(suite.T(), Define(c, opts))

err := c.RegisterFlagCompletionFunc("region", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"manual"}, cobra.ShellCompDirectiveDefault
})
require.Error(suite.T(), err)
assert.Contains(suite.T(), err.Error(), "already registered")
}

type ignoreCompletionOptions struct {
Hidden string `flag:"hidden" flagignore:"true"`
Visible string `flag:"visible"`
Expand Down
63 changes: 63 additions & 0 deletions errors/errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,69 @@ func TestInvalidDefineHookSignatureError_ErrorsAs(t *testing.T) {
assert.Equal(t, "TestField", fieldErr.Field())
}

func TestInvalidCompleteHookSignatureError_ErrorMessage(t *testing.T) {
err := &InvalidCompleteHookSignatureError{
FieldName: "ServerMode",
HookName: "CompleteServerMode",
Message: "complete hook must have signature: func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective)",
}

expected := "field 'ServerMode': invalid 'CompleteServerMode' completion hook: complete hook must have signature: func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective)"
assert.Equal(t, expected, err.Error())
}

func TestInvalidCompleteHookSignatureError_ContainsExpectedStrings(t *testing.T) {
err := &InvalidCompleteHookSignatureError{
FieldName: "CustomField",
HookName: "CompleteCustomField",
Message: "wrong return type",
}

errorMsg := err.Error()
assert.Contains(t, errorMsg, "CustomField")
assert.Contains(t, errorMsg, "CompleteCustomField")
assert.Contains(t, errorMsg, "wrong return type")
assert.Contains(t, errorMsg, "completion hook")
}

func TestInvalidCompleteHookSignatureError_FieldInterface(t *testing.T) {
err := &InvalidCompleteHookSignatureError{
FieldName: "TestField",
HookName: "CompleteTestField",
Message: "invalid signature",
}

var fieldErr DefinitionError = err
assert.Equal(t, "TestField", fieldErr.Field())
}

func TestInvalidCompleteHookSignatureError_ErrorsIs(t *testing.T) {
err := &InvalidCompleteHookSignatureError{
FieldName: "TestField",
HookName: "CompleteTestField",
Message: "invalid signature",
}

assert.True(t, errors.Is(err, ErrInvalidCompleteHookSignature))
assert.False(t, errors.Is(err, ErrInvalidDefineHookSignature))
assert.False(t, errors.Is(err, ErrInvalidDecodeHookSignature))
}

func TestInvalidCompleteHookSignatureError_ErrorsAs(t *testing.T) {
originalErr := errors.New("parameter 2 has wrong type")
err := NewInvalidCompleteHookSignatureError("TestField", "CompleteTestField", originalErr)

var completeErr *InvalidCompleteHookSignatureError
require.True(t, errors.As(err, &completeErr))
assert.Equal(t, "TestField", completeErr.FieldName)
assert.Equal(t, "CompleteTestField", completeErr.HookName)
assert.Equal(t, "parameter 2 has wrong type", completeErr.Message)

var fieldErr DefinitionError
require.True(t, errors.As(err, &fieldErr))
assert.Equal(t, "TestField", fieldErr.Field())
}

func TestConflictingTagsError_ErrorMessage(t *testing.T) {
err := &ConflictingTagsError{
FieldName: "TestField",
Expand Down
54 changes: 54 additions & 0 deletions integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,60 @@ func (o TestDefineOptions) Attach(c *cobra.Command) error { return nil }
func (o TestDefineOptions) Transform(ctx context.Context) error { return nil }
func (o TestDefineOptions) Validate() []error { return nil }

type completionIntegrationOptions struct {
Region string `flag:"region" flagdescr:"target region"`
}

func (o *completionIntegrationOptions) Attach(c *cobra.Command) error { return nil }

func (o *completionIntegrationOptions) CompleteRegion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
candidates := []string{"us-east-1", "us-west-2", "eu-west-1"}
if toComplete == "" {
return candidates, cobra.ShellCompDirectiveNoFileComp
}

filtered := make([]string, 0, len(candidates))
for _, candidate := range candidates {
if strings.HasPrefix(candidate, toComplete) {
filtered = append(filtered, candidate)
}
}

return filtered, cobra.ShellCompDirectiveNoFileComp
}

func TestDefine_CompletionShellRequest_Integration(t *testing.T) {
viper.Reset()
structcli.Reset()
defer viper.Reset()
defer structcli.Reset()

opts := &completionIntegrationOptions{}

runCmd := &cobra.Command{
Use: "run",
Run: func(cmd *cobra.Command, args []string) {},
}
require.NoError(t, structcli.Define(runCmd, opts))

rootCmd := &cobra.Command{Use: "app"}
rootCmd.AddCommand(runCmd)

var out bytes.Buffer
rootCmd.SetOut(&out)
rootCmd.SetErr(&out)
rootCmd.SetArgs([]string{"__complete", "run", "--region", "us"})

err := rootCmd.Execute()
require.NoError(t, err)

output := out.String()
assert.Contains(t, output, "us-east-1")
assert.Contains(t, output, "us-west-2")
assert.NotContains(t, output, "eu-west-1")
assert.Contains(t, output, ":4")
}

func TestDefine_Integration(t *testing.T) {
setupTest := func() {
viper.Reset()
Expand Down
9 changes: 9 additions & 0 deletions internal/hooks/hooks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,15 @@ func (suite *structcliSuite) TestStoreCompletionHookFunc() {
assert.Equal(suite.T(), cobra.ShellCompDirectiveNoFileComp, directive)
}

func (suite *structcliSuite) TestStoreCompletionHookFunc_InvalidHookValue() {
cmd := &cobra.Command{Use: "testcmd"}
cmd.Flags().String("mode", "", "test flag")

err := internalhooks.StoreCompletionHookFunc(cmd, "mode", reflect.Value{})
require.Error(suite.T(), err)
assert.Contains(suite.T(), err.Error(), "invalid completion hook")
}

type zapcoreLevelOptions struct {
LogLevel zapcore.Level `default:"info" flagcustom:"true" flagdescr:"the logging level" flagenv:"true"`
}
Expand Down