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
1 change: 1 addition & 0 deletions cmd/cliflags/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const (
EmailsFlag = "emails"
EnvironmentFlag = "environment"
FlagFlag = "flag"
OutputFlag = "output"
ProjectFlag = "project"
RoleFlag = "role"
)
1 change: 1 addition & 0 deletions cmd/config/testdata/help.golden
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ Flags:
Global Flags:
--access-token string LaunchDarkly API token with write-level access
--base-uri string LaunchDarkly base URI (default "https://app.launchdarkly.com")
-o, --output string Command response output format in either JSON or plain text (default "plaintext")
11 changes: 10 additions & 1 deletion cmd/environments/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"ldcli/cmd/cliflags"
"ldcli/cmd/validators"
"ldcli/internal/environments"
"ldcli/internal/output"
)

func NewGetCmd(
Expand Down Expand Up @@ -65,7 +66,15 @@ func runGet(
return err
}

fmt.Fprintf(cmd.OutOrStdout(), string(response)+"\n")
output, err := output.CmdOutput(
viper.GetString(cliflags.OutputFlag),
output.NewSingularOutputterFn(response),
)
if err != nil {
return err
}

fmt.Fprintf(cmd.OutOrStdout(), string(output)+"\n")

return nil
}
Expand Down
33 changes: 27 additions & 6 deletions cmd/environments/get_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,29 @@ func TestGet(t *testing.T) {
"test-env",
"test-proj",
}
stubbedResponse := `{"key": "test-key", "name": "test-name"}`

t.Run("with valid environments calls API", func(t *testing.T) {
t.Run("with valid flags calls API", func(t *testing.T) {
client := environments.MockClient{}
client.
On("Get", mockArgs...).
Return([]byte(cmd.ValidResponse), nil)
Return([]byte(stubbedResponse), nil)
clients := cmd.APIClients{
EnvironmentsClient: &client,
}
args := []string{
"environments", "get",
"--access-token", "testAccessToken",
"--base-uri", "http://test.com",
"--output", "json",
"--environment", "test-env",
"--project", "test-proj",
}

output, err := cmd.CallCmd(t, clients, &analytics.NoopClient{}, args)

require.NoError(t, err)
assert.JSONEq(t, `{"valid": true}`, string(output))
assert.JSONEq(t, stubbedResponse, string(output))
})

t.Run("with valid flags from environment variables calls API", func(t *testing.T) {
Expand All @@ -49,20 +51,21 @@ func TestGet(t *testing.T) {
client := environments.MockClient{}
client.
On("Get", mockArgs...).
Return([]byte(cmd.ValidResponse), nil)
Return([]byte(stubbedResponse), nil)
clients := cmd.APIClients{
EnvironmentsClient: &client,
}
args := []string{
"environments", "get",
"--output", "json",
"--environment", "test-env",
"--project", "test-proj",
}

output, err := cmd.CallCmd(t, clients, &analytics.NoopClient{}, args)

require.NoError(t, err)
assert.JSONEq(t, `{"valid": true}`, string(output))
assert.JSONEq(t, stubbedResponse, string(output))
})

t.Run("with an error response is an error", func(t *testing.T) {
Expand All @@ -77,6 +80,7 @@ func TestGet(t *testing.T) {
"environments", "get",
"--access-token", "testAccessToken",
"--base-uri", "http://test.com",
"--output", "json",
"--environment", "test-env",
"--project", "test-proj",
}
Expand Down Expand Up @@ -144,6 +148,23 @@ func TestGet(t *testing.T) {
assert.EqualError(t, err, "base-uri is invalid"+errorHelp)
})

t.Run("with invalid output is an error", func(t *testing.T) {
clients := cmd.APIClients{
EnvironmentsClient: &environments.MockClient{},
}
args := []string{
"environments", "get",
"--access-token", "testAccessToken",
"--output", "invalid",
"--environment", "test-env",
"--project", "test-proj",
}

_, err := cmd.CallCmd(t, clients, &analytics.NoopClient{}, args)

assert.EqualError(t, err, "output is invalid"+errorHelp)
})

t.Run("will track analytics for CLI Command Run event", func(t *testing.T) {
tracker := analytics.MockedTracker(
"environments",
Expand All @@ -154,7 +175,6 @@ func TestGet(t *testing.T) {
"environment",
"project",
})

client := environments.MockClient{}
client.
On("Get", mockArgs...).
Expand All @@ -172,6 +192,7 @@ func TestGet(t *testing.T) {
}

_, err := cmd.CallCmd(t, clients, tracker, args)

require.NoError(t, err)
})
}
11 changes: 10 additions & 1 deletion cmd/projects/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"ldcli/cmd/cliflags"
"ldcli/cmd/validators"
"ldcli/internal/output"
"ldcli/internal/projects"
)

Expand All @@ -35,7 +36,15 @@ func runList(client projects.Client) func(*cobra.Command, []string) error {
return err
}

fmt.Fprintf(cmd.OutOrStdout(), string(response)+"\n")
output, err := output.CmdOutput(
viper.GetString(cliflags.OutputFlag),
output.NewMultipleOutputterFn(response),
)
if err != nil {
return err
}

fmt.Fprintf(cmd.OutOrStdout(), string(output)+"\n")

return nil
}
Expand Down
18 changes: 14 additions & 4 deletions cmd/projects/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,34 @@ func TestList(t *testing.T) {
"testAccessToken",
"http://test.com",
}
stubbedResponse := `{
"items": [
{
"key": "test-key",
"name": "test-name"
}
]
}`

t.Run("with valid flags calls API", func(t *testing.T) {
client := projects.MockClient{}
client.
On("List", mockArgs...).
Return([]byte(cmd.ValidResponse), nil)
Return([]byte(stubbedResponse), nil)
clients := cmd.APIClients{
ProjectsClient: &client,
}
args := []string{
"projects", "list",
"--access-token", "testAccessToken",
"--base-uri", "http://test.com",
"--output", "json",
}

output, err := cmd.CallCmd(t, clients, &analytics.NoopClient{}, args)

require.NoError(t, err)
assert.JSONEq(t, `{"valid": true}`, string(output))
assert.JSONEq(t, stubbedResponse, string(output))
})

t.Run("with valid flags from environment variables calls API", func(t *testing.T) {
Expand All @@ -45,19 +54,20 @@ func TestList(t *testing.T) {
client := projects.MockClient{}
client.
On("List", mockArgs...).
Return([]byte(cmd.ValidResponse), nil)
Return([]byte(stubbedResponse), nil)
clients := cmd.APIClients{
ProjectsClient: &client,
}
args := []string{
"projects",
"list",
"--output", "json",
}

output, err := cmd.CallCmd(t, clients, &analytics.NoopClient{}, args)

require.NoError(t, err)
assert.JSONEq(t, `{"valid": true}`, string(output))
assert.JSONEq(t, stubbedResponse, string(output))
})

t.Run("with an error response is an error", func(t *testing.T) {
Expand Down
11 changes: 11 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,17 @@ func NewRootCommand(
return nil, err
}

cmd.PersistentFlags().StringP(
cliflags.OutputFlag,
"o",
"plaintext",
"Command response output format in either JSON or plain text",
)
err = viper.BindPFlag(cliflags.OutputFlag, cmd.PersistentFlags().Lookup(cliflags.OutputFlag))
if err != nil {
return nil, err
}

environmentsCmd, err := envscmd.NewEnvironmentsCmd(analyticsTracker, clients.EnvironmentsClient)
if err != nil {
return nil, err
Expand Down
46 changes: 26 additions & 20 deletions cmd/validators/validators.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,48 +11,54 @@ import (

"ldcli/cmd/cliflags"
errs "ldcli/internal/errors"
"ldcli/internal/output"
)

// Validate is a validator for commands to print an error when the user input is invalid.
func Validate() cobra.PositionalArgs {
return func(cmd *cobra.Command, args []string) error {
rebindFlags(cmd, cmd.ValidArgs) // rebind flags before validating them below
commandPath := getCommandPath(cmd)

_, err := url.ParseRequestURI(viper.GetString(cliflags.BaseURIFlag))
if err != nil {
errorMessage := fmt.Sprintf(
"%s. See `%s --help` for supported flags and usage.",
errs.ErrInvalidBaseURI,
commandPath,
)
return errors.New(errorMessage)
return CmdError(errs.ErrInvalidBaseURI, cmd.CommandPath())
}

err = cmd.ValidateRequiredFlags()
if err != nil {
errorMessage := fmt.Sprintf(
"%s. See `%s --help` for supported flags and usage.",
err.Error(),
commandPath,
)
return CmdError(err, cmd.CommandPath())
}

return errors.New(errorMessage)
err = validateOutput(viper.GetString(cliflags.OutputFlag))
if err != nil {
return CmdError(err, cmd.CommandPath())
}

return nil
}
}

func getCommandPath(cmd *cobra.Command) string {
var commandPath string
if cmd.Annotations["scope"] == "plugin" {
commandPath = fmt.Sprintf("stripe %s", cmd.CommandPath())
} else {
commandPath = cmd.CommandPath()
func CmdError(err error, commandPath string) error {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not related to this change but in getCommandPath we have stripe hardcoded still 😅

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should remove this reference haha

errorMessage := fmt.Sprintf(
"%s. See `%s --help` for supported flags and usage.",
err.Error(),
commandPath,
)

return errors.New(errorMessage)
}

func validateOutput(outputFlag string) error {
validKinds := map[string]struct{}{
"json": {},
"plaintext": {},
}
_, ok := validKinds[outputFlag]
if !ok {
return output.ErrInvalidOutputKind
}

return commandPath
return nil
}

// rebindFlags sets the command's flags based on the values stored in viper because they may not
Expand Down
7 changes: 4 additions & 3 deletions internal/environments/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,11 @@ func (c EnvironmentsClient) Get(

}

responseJSON, err := json.Marshal(environment)
output, err := json.Marshal(environment)
if err != nil {
return nil, err
return nil, errors.NewLDAPIError(err)

}

return responseJSON, nil
return output, nil
}
49 changes: 49 additions & 0 deletions internal/output/multiple_outputter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package output

import (
"encoding/json"
"fmt"
)

var multiplePlaintextOutputFn = func(r resource) string {
return fmt.Sprintf("* %s (%s)", r.Name, r.Key)
}

// TODO: rename this to be "cleaner"? -- NewMultipleOutput()
func NewMultipleOutputterFn(input []byte) multipleOutputterFn {
return multipleOutputterFn{
input: input,
}
}

type multipleOutputterFn struct {
input []byte
}

func (o multipleOutputterFn) New() (Outputter, error) {
var r resources
err := json.Unmarshal(o.input, &r)
if err != nil {
return MultipleOutputter{}, err
}

return MultipleOutputter{
outputFn: multiplePlaintextOutputFn,
resources: r,
resourceJSON: o.input,
}, nil
}

type MultipleOutputter struct {
outputFn PlaintextOutputFn
resources resources
resourceJSON []byte
}

func (o MultipleOutputter) JSON() string {
return string(o.resourceJSON)
}

func (o MultipleOutputter) String() string {
return formatColl(o.resources.Items, o.outputFn)
}
Loading