Skip to content

Commit

Permalink
Add Command.SetContext
Browse files Browse the repository at this point in the history
Basically the same as spf13/cobra#1517 but
uses the `Set<field>` naming convention instead of `WithContext`

Context setting without execution is important because it means
that more design patterns can be achieved.

Currently I am using functional options in a project and I can add
behaviour through functional options as such:
```go
type GrptlOption func(*cobra.Command) error

func WithFileDescriptors(descriptors ...protoreflect.FileDescriptor) GrptlOption {
	return func(cmd *cobra.Command) error {
		err := CommandFromFileDescriptors(cmd, descriptors...)
		if err != nil {
			return err
		}
		return nil
	}
}
```

I've got a lot more options and this pattern allows me to have nice
abstracted pieces of logic that interact with the cobra command.

This Pattern also allows for adding extra information to a call
through `PreRun` functions:

```go

cmd := &cobra.Command{
		Use: "Foobar",
		PersistentPreRun: func(cmd *cobra.Command, args []string) {
			err :=cmd.SetContext(metadata.AppendToOutgoingContext(context.Background(), "pre", "run"))
		},
	}
```

This is a veer nice abstraction and allows for these functions to be
very modular

The issue I'm facing at the moment is that I can't write a nifty
option (something like `WithAuthentication`) because that needs access
to reading and setting the context. Currently I can only read the
context.

Needing to use `ExecuteContext` breaks this abstraction because I need
to run it right at the end.

Merge spf13/cobra#1551

Fixes spf13/cobra#1517
Fixes spf13/cobra#1118
Fixes spf13/cobra#563
  • Loading branch information
joshcarp authored and hoshsadiq committed Feb 8, 2022
1 parent e55aff1 commit 2f702d2
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 0 deletions.
6 changes: 6 additions & 0 deletions command.go
Expand Up @@ -233,6 +233,12 @@ func (c *Command) Context() context.Context {
return c.ctx
}

// SetContext sets context for the command. It is set to context.Background by default and will be overwritten by
// Command.ExecuteContext or Command.ExecuteContextC
func (c *Command) SetContext(ctx context.Context) {
c.ctx = ctx
}

// SetArgs sets arguments for the command. It is set to os.Args[1:] by default, if desired, can be overridden
// particularly useful when testing.
func (c *Command) SetArgs(a []string) {
Expand Down
98 changes: 98 additions & 0 deletions command_test.go
Expand Up @@ -2087,3 +2087,101 @@ func TestContext(t *testing.T){
t.Error("expected root.Context() != nil")
}
}

func TestSetContext(t *testing.T) {
key, val := "foo", "bar"
root := &Command{
Use: "root",
Run: func(cmd *Command, args []string) {
key := cmd.Context().Value(key)
got, ok := key.(string)
if !ok {
t.Error("key not found in context")
}
if got != val {
t.Errorf("Expected value: \n %v\nGot:\n %v\n", val, got)
}
},
}
ctx := context.WithValue(context.Background(), key, val)
root.SetContext(ctx)
err := root.Execute()
if err != nil {
t.Error(err)
}
}

func TestSetContextPreRun(t *testing.T) {
key, val := "foo", "bar"
root := &Command{
Use: "root",
PreRun: func(cmd *Command, args []string) {
ctx := context.WithValue(cmd.Context(), key, val)
cmd.SetContext(ctx)
},
Run: func(cmd *Command, args []string) {
key := cmd.Context().Value(key)
got, ok := key.(string)
if !ok {
t.Error("key not found in context")
}
if got != val {
t.Errorf("Expected value: \n %v\nGot:\n %v\n", val, got)
}
},
}
err := root.Execute()
if err != nil {
t.Error(err)
}
}

func TestSetContextPreRunOverwrite(t *testing.T) {
key, val := "foo", "bar"
root := &Command{
Use: "root",
Run: func(cmd *Command, args []string) {
key := cmd.Context().Value(key)
_, ok := key.(string)
if ok {
t.Error("key found in context when not expected")
}
},
}
ctx := context.WithValue(context.Background(), key, val)
root.SetContext(ctx)
err := root.ExecuteContext(context.Background())
if err != nil {
t.Error(err)
}
}

func TestSetContextPersistentPreRun(t *testing.T) {
key, val := "foo", "bar"
root := &Command{
Use: "root",
PersistentPreRun: func(cmd *Command, args []string) {
ctx := context.WithValue(cmd.Context(), key, val)
cmd.SetContext(ctx)
},
}
child := &Command{
Use: "child",
Run: func(cmd *Command, args []string) {
key := cmd.Context().Value(key)
got, ok := key.(string)
if !ok {
t.Error("key not found in context")
}
if got != val {
t.Errorf("Expected value: \n %v\nGot:\n %v\n", val, got)
}
},
}
root.AddCommand(child)
root.SetArgs([]string{"child"})
err := root.Execute()
if err != nil {
t.Error(err)
}
}

0 comments on commit 2f702d2

Please sign in to comment.