From ae51430a5249bdcd5d6890b9f824c71cd2b19fc2 Mon Sep 17 00:00:00 2001 From: Kelly Hofmann Date: Thu, 25 Apr 2024 13:46:56 -0700 Subject: [PATCH 01/10] format url with url params --- cmd/resources/resources.go | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/cmd/resources/resources.go b/cmd/resources/resources.go index e3de364c..6e5ff92e 100644 --- a/cmd/resources/resources.go +++ b/cmd/resources/resources.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "net/http" + "regexp" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -87,6 +88,7 @@ func (op *OperationCmd) initFlags() error { if err != nil { return err } + } err := viper.BindPFlag(p.Name, op.cmd.Flags().Lookup(p.Name)) @@ -97,9 +99,20 @@ func (op *OperationCmd) initFlags() error { return nil } +func formatURL(path string, urlParams []string) string { + s := make([]interface{}, len(urlParams)) + for i, v := range urlParams { + s[i] = v + } + + re := regexp.MustCompile(`{\w+}`) + format := re.ReplaceAllString(path, "%s") + + return fmt.Sprintf(format, s...) +} + func (op *OperationCmd) makeRequest(cmd *cobra.Command, args []string) error { paramVals := map[string]interface{}{} - if op.RequiresBody { var data interface{} // TODO: why does viper.GetString(cliflags.DataFlag) not work? @@ -110,6 +123,7 @@ func (op *OperationCmd) makeRequest(cmd *cobra.Command, args []string) error { paramVals[cliflags.DataFlag] = data } + var urlParms []string for _, p := range op.Params { var val interface{} switch p.Type { @@ -123,10 +137,15 @@ func (op *OperationCmd) makeRequest(cmd *cobra.Command, args []string) error { if val != nil { paramVals[p.Name] = val + if p.In == "path" { + urlParms = append(urlParms, fmt.Sprintf("%v", val)) + } } } - fmt.Fprintf(cmd.OutOrStdout(), "would be making a %s request to %s here, with args: %s\n", op.HTTPMethod, op.Path, paramVals) + path := formatURL(op.Path, urlParms) + + fmt.Fprintf(cmd.OutOrStdout(), "would be making a %s request to %s here, with args: %s\n", op.HTTPMethod, path, paramVals) return nil } From 590d2867ba9d305c41e333bd13648e3efe3d9eee Mon Sep 17 00:00:00 2001 From: Kelly Hofmann Date: Thu, 25 Apr 2024 14:56:39 -0700 Subject: [PATCH 02/10] make a real request --- cmd/resources/resources.go | 87 ++++++++++++++++++++++++-------------- 1 file changed, 56 insertions(+), 31 deletions(-) diff --git a/cmd/resources/resources.go b/cmd/resources/resources.go index 6e5ff92e..36a95e77 100644 --- a/cmd/resources/resources.go +++ b/cmd/resources/resources.go @@ -1,13 +1,15 @@ package resources import ( + "bytes" "encoding/json" "fmt" - "net/http" - "regexp" - "github.com/spf13/cobra" "github.com/spf13/viper" + "net/http" + "net/url" + "regexp" + "strings" cmdAnalytics "ldcli/cmd/analytics" "ldcli/cmd/cliflags" @@ -36,13 +38,14 @@ func NewResourceCmd(parentCmd *cobra.Command, analyticsTracker analytics.Tracker } type OperationData struct { - Short string - Long string - Use string - Params []Param - HTTPMethod string - RequiresBody bool - Path string + Short string + Long string + Use string + Params []Param + HTTPMethod string + RequiresBody bool + Path string + SupportsSemanticPatch bool // TBD on how to actually determine from openapi spec } type Param struct { @@ -72,8 +75,17 @@ func (op *OperationCmd) initFlags() error { } } + if op.SupportsSemanticPatch { + op.cmd.Flags().Bool("semantic-patch", false, "Perform a semantic patch request") + err := viper.BindPFlag("semantic-patch", op.cmd.Flags().Lookup("semantic-patch")) + if err != nil { + return err + } + } + for _, p := range op.Params { shorthand := fmt.Sprintf(p.Name[0:1]) // todo: how do we handle potential dupes + // TODO: consider handling these all as strings switch p.Type { case "string": op.cmd.Flags().StringP(p.Name, shorthand, "", p.Description) @@ -88,7 +100,6 @@ func (op *OperationCmd) initFlags() error { if err != nil { return err } - } err := viper.BindPFlag(p.Name, op.cmd.Flags().Lookup(p.Name)) @@ -99,7 +110,7 @@ func (op *OperationCmd) initFlags() error { return nil } -func formatURL(path string, urlParams []string) string { +func formatURL(baseURI, path string, urlParams []string) string { s := make([]interface{}, len(urlParams)) for i, v := range urlParams { s[i] = v @@ -108,44 +119,58 @@ func formatURL(path string, urlParams []string) string { re := regexp.MustCompile(`{\w+}`) format := re.ReplaceAllString(path, "%s") - return fmt.Sprintf(format, s...) + return baseURI + fmt.Sprintf(format, s...) } func (op *OperationCmd) makeRequest(cmd *cobra.Command, args []string) error { - paramVals := map[string]interface{}{} + var data interface{} if op.RequiresBody { - var data interface{} // TODO: why does viper.GetString(cliflags.DataFlag) not work? err := json.Unmarshal([]byte(cmd.Flags().Lookup(cliflags.DataFlag).Value.String()), &data) if err != nil { return err } - paramVals[cliflags.DataFlag] = data } + query := url.Values{} var urlParms []string for _, p := range op.Params { - var val interface{} - switch p.Type { - case "string": - val = viper.GetString(p.Name) - case "boolean": - val = viper.GetBool(p.Name) - case "int": - val = viper.GetInt(p.Name) - } - - if val != nil { - paramVals[p.Name] = val - if p.In == "path" { + val := viper.GetString(p.Name) + if val != "" { + switch p.In { + case "path": urlParms = append(urlParms, fmt.Sprintf("%v", val)) + case "query": + query.Add(p.Name, fmt.Sprintf("%v", val)) } } } - path := formatURL(op.Path, urlParms) + contentType := "application/json" + if op.SupportsSemanticPatch && viper.GetBool("semantic-patch") { + contentType += "; domain-model=launchdarkly.semanticpatch" + } + + path := formatURL(viper.GetString(cliflags.BaseURIFlag), op.Path, urlParms) + + jsonData, err := json.Marshal(data) + if err != nil { + return err + } + + req, _ := http.NewRequest(strings.ToUpper(op.HTTPMethod), path, bytes.NewReader(jsonData)) + req.Header.Add("Authorization", viper.GetString(cliflags.AccessTokenFlag)) + req.Header.Add("Content-type", contentType) + req.URL.RawQuery = query.Encode() + + res, err := op.client.Do(req) + if err != nil { + return err + } + defer res.Body.Close() - fmt.Fprintf(cmd.OutOrStdout(), "would be making a %s request to %s here, with args: %s\n", op.HTTPMethod, path, paramVals) + // TODO replace with outputter, handle errors + fmt.Fprintf(cmd.OutOrStdout(), "would be making a %s request to %s?%s\n", op.HTTPMethod, path, query.Encode()) return nil } From 1caa24992e1ccc111d99ce05acf92b61d858263c Mon Sep 17 00:00:00 2001 From: Kelly Hofmann Date: Thu, 25 Apr 2024 16:30:51 -0700 Subject: [PATCH 03/10] handle singular outputs only for now --- cmd/resource_cmds.go | 9 +++++---- cmd/resources/resources.go | 35 +++++++++++++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/cmd/resource_cmds.go b/cmd/resource_cmds.go index 2d811b97..c969d7f0 100644 --- a/cmd/resource_cmds.go +++ b/cmd/resource_cmds.go @@ -3,6 +3,7 @@ package cmd import ( + "ldcli/internal/output" "net/http" "github.com/spf13/cobra" @@ -34,11 +35,11 @@ func addAllResourceCmds(rootCmd *cobra.Command, client *http.Client, analyticsTr Type: "string", }, }, - HTTPMethod: "post", - RequiresBody: true, - Path: "/api/v2/teams", + HTTPMethod: "post", + RequiresBody: true, + Path: "/api/v2/teams", + PlaintextOutputFn: output.SingularPlaintextOutputFn, }) - resources.NewOperationCmd(gen_TeamsResourceCmd, client, resources.OperationData{ Short: "Get team", Long: "Fetch a team by key.\n\n### Expanding the teams response\nLaunchDarkly supports four fields for expanding the \"Get team\" response. By default, these fields are **not** included in the response.\n\nTo expand the response, append the `expand` query parameter and add a comma-separated list with any of the following fields:\n\n* `members` includes the total count of members that belong to the team.\n* `roles` includes a paginated list of the custom roles that you have assigned to the team.\n* `projects` includes a paginated list of the projects that the team has any write access to.\n* `maintainers` includes a paginated list of the maintainers that you have assigned to the team.\n\nFor example, `expand=members,roles` includes the `members` and `roles` fields in the response.\n", diff --git a/cmd/resources/resources.go b/cmd/resources/resources.go index 36a95e77..5a2d17aa 100644 --- a/cmd/resources/resources.go +++ b/cmd/resources/resources.go @@ -6,6 +6,9 @@ import ( "fmt" "github.com/spf13/cobra" "github.com/spf13/viper" + "io" + "ldcli/internal/errors" + "ldcli/internal/output" "net/http" "net/url" "regexp" @@ -46,6 +49,7 @@ type OperationData struct { RequiresBody bool Path string SupportsSemanticPatch bool // TBD on how to actually determine from openapi spec + PlaintextOutputFn output.PlaintextOutputFn } type Param struct { @@ -169,8 +173,35 @@ func (op *OperationCmd) makeRequest(cmd *cobra.Command, args []string) error { } defer res.Body.Close() - // TODO replace with outputter, handle errors - fmt.Fprintf(cmd.OutOrStdout(), "would be making a %s request to %s?%s\n", op.HTTPMethod, path, query.Encode()) + body, err := io.ReadAll(res.Body) + if err != nil { + return err + } + + if res.StatusCode >= 400 { + err = errors.NewAPIError(body, nil, nil) + output, err := output.CmdOutputSingular( + viper.GetString(cliflags.OutputFlag), + body, + output.ErrorPlaintextOutputFn, + ) + if err != nil { + return errors.NewError(err.Error()) + } + + return errors.NewError(output) + } + + output, err := output.CmdOutputSingular( + viper.GetString(cliflags.OutputFlag), + body, + op.PlaintextOutputFn, + ) + if err != nil { + return errors.NewError(err.Error()) + } + + fmt.Fprintf(cmd.OutOrStdout(), output+"\n") return nil } From ab73842d2195bed4c8ccfd45f0e46df1cb77c143 Mon Sep 17 00:00:00 2001 From: Kelly Hofmann Date: Mon, 29 Apr 2024 15:38:09 -0700 Subject: [PATCH 04/10] refactor client --- cmd/{ => resources}/resource_cmds.go | 20 +++--- cmd/{ => resources}/resource_cmds_test.go | 6 ++ cmd/resources/resources.go | 74 +++++++++-------------- cmd/root.go | 10 +-- internal/resources/client.go | 53 ++++++++++++++++ internal/resources/mock_client.go | 1 + 6 files changed, 101 insertions(+), 63 deletions(-) rename cmd/{ => resources}/resource_cmds.go (89%) rename cmd/{ => resources}/resource_cmds_test.go (85%) create mode 100644 internal/resources/client.go create mode 100644 internal/resources/mock_client.go diff --git a/cmd/resource_cmds.go b/cmd/resources/resource_cmds.go similarity index 89% rename from cmd/resource_cmds.go rename to cmd/resources/resource_cmds.go index c969d7f0..c9fa6f38 100644 --- a/cmd/resource_cmds.go +++ b/cmd/resources/resource_cmds.go @@ -1,20 +1,18 @@ // this file WILL be generated (sc-241153) -package cmd +package resources import ( - "ldcli/internal/output" - "net/http" - "github.com/spf13/cobra" - "ldcli/cmd/resources" "ldcli/internal/analytics" + "ldcli/internal/output" + "ldcli/internal/resources" ) -func addAllResourceCmds(rootCmd *cobra.Command, client *http.Client, analyticsTracker analytics.Tracker) { +func AddAllResourceCmds(rootCmd *cobra.Command, client resources.Client, analyticsTracker analytics.Tracker) { // Resource commands - gen_TeamsResourceCmd := resources.NewResourceCmd( + gen_TeamsResourceCmd := NewResourceCmd( rootCmd, analyticsTracker, "teams", @@ -23,11 +21,11 @@ func addAllResourceCmds(rootCmd *cobra.Command, client *http.Client, analyticsTr ) // Operation commands - resources.NewOperationCmd(gen_TeamsResourceCmd, client, resources.OperationData{ + NewOperationCmd(gen_TeamsResourceCmd, client, OperationData{ Short: "Create team", Long: "Create a team. To learn more, read [Creating a team](https://docs.launchdarkly.com/home/teams/creating).\n\n### Expanding the teams response\nLaunchDarkly supports four fields for expanding the \"Create team\" response. By default, these fields are **not** included in the response.\n\nTo expand the response, append the `expand` query parameter and add a comma-separated list with any of the following fields:\n\n* `members` includes the total count of members that belong to the team.\n* `roles` includes a paginated list of the custom roles that you have assigned to the team.\n* `projects` includes a paginated list of the projects that the team has any write access to.\n* `maintainers` includes a paginated list of the maintainers that you have assigned to the team.\n\nFor example, `expand=members,roles` includes the `members` and `roles` fields in the response.\n", Use: "create", // TODO: translate post -> create - Params: []resources.Param{ + Params: []Param{ { Name: "expand", In: "query", @@ -40,11 +38,11 @@ func addAllResourceCmds(rootCmd *cobra.Command, client *http.Client, analyticsTr Path: "/api/v2/teams", PlaintextOutputFn: output.SingularPlaintextOutputFn, }) - resources.NewOperationCmd(gen_TeamsResourceCmd, client, resources.OperationData{ + NewOperationCmd(gen_TeamsResourceCmd, client, OperationData{ Short: "Get team", Long: "Fetch a team by key.\n\n### Expanding the teams response\nLaunchDarkly supports four fields for expanding the \"Get team\" response. By default, these fields are **not** included in the response.\n\nTo expand the response, append the `expand` query parameter and add a comma-separated list with any of the following fields:\n\n* `members` includes the total count of members that belong to the team.\n* `roles` includes a paginated list of the custom roles that you have assigned to the team.\n* `projects` includes a paginated list of the projects that the team has any write access to.\n* `maintainers` includes a paginated list of the maintainers that you have assigned to the team.\n\nFor example, `expand=members,roles` includes the `members` and `roles` fields in the response.\n", Use: "get", - Params: []resources.Param{ + Params: []Param{ { Name: "teamKey", // TODO: kebab case/trim key? to be consistent with our existing flags (e.g. projectKey = project) In: "path", diff --git a/cmd/resource_cmds_test.go b/cmd/resources/resource_cmds_test.go similarity index 85% rename from cmd/resource_cmds_test.go rename to cmd/resources/resource_cmds_test.go index 48e8e509..b15847b2 100644 --- a/cmd/resource_cmds_test.go +++ b/cmd/resources/resource_cmds_test.go @@ -1,6 +1,8 @@ package cmd_test import ( + "github.com/stretchr/testify/mock" + resources "ldcli/internal/resources" "testing" "github.com/stretchr/testify/assert" @@ -24,6 +26,10 @@ func TestCreateTeam(t *testing.T) { assert.Contains(t, string(output), "Create a team.") }) t.Run("with valid flags calls makeRequest function", func(t *testing.T) { + client := resources.MockClient{} + client. + On("MakeRequest", mock.Anything). + Return() args := []string{ "teams", "create", diff --git a/cmd/resources/resources.go b/cmd/resources/resources.go index 5a2d17aa..0fa94230 100644 --- a/cmd/resources/resources.go +++ b/cmd/resources/resources.go @@ -1,23 +1,22 @@ package resources import ( - "bytes" "encoding/json" "fmt" - "github.com/spf13/cobra" - "github.com/spf13/viper" - "io" - "ldcli/internal/errors" - "ldcli/internal/output" - "net/http" "net/url" "regexp" "strings" + "github.com/spf13/cobra" + "github.com/spf13/viper" + cmdAnalytics "ldcli/cmd/analytics" "ldcli/cmd/cliflags" "ldcli/cmd/validators" "ldcli/internal/analytics" + "ldcli/internal/errors" + "ldcli/internal/output" + "ldcli/internal/resources" ) func NewResourceCmd(parentCmd *cobra.Command, analyticsTracker analytics.Tracker, resourceName, shortDescription, longDescription string) *cobra.Command { @@ -62,7 +61,7 @@ type Param struct { type OperationCmd struct { OperationData - client *http.Client + client resources.Client cmd *cobra.Command } @@ -135,6 +134,10 @@ func (op *OperationCmd) makeRequest(cmd *cobra.Command, args []string) error { return err } } + jsonData, err := json.Marshal(data) + if err != nil { + return err + } query := url.Values{} var urlParms []string @@ -150,63 +153,41 @@ func (op *OperationCmd) makeRequest(cmd *cobra.Command, args []string) error { } } + path := formatURL(viper.GetString(cliflags.BaseURIFlag), op.Path, urlParms) + contentType := "application/json" if op.SupportsSemanticPatch && viper.GetBool("semantic-patch") { contentType += "; domain-model=launchdarkly.semanticpatch" } - path := formatURL(viper.GetString(cliflags.BaseURIFlag), op.Path, urlParms) - - jsonData, err := json.Marshal(data) - if err != nil { - return err - } - - req, _ := http.NewRequest(strings.ToUpper(op.HTTPMethod), path, bytes.NewReader(jsonData)) - req.Header.Add("Authorization", viper.GetString(cliflags.AccessTokenFlag)) - req.Header.Add("Content-type", contentType) - req.URL.RawQuery = query.Encode() - - res, err := op.client.Do(req) - if err != nil { - return err - } - defer res.Body.Close() - - body, err := io.ReadAll(res.Body) + res, err := op.client.MakeRequest( + viper.GetString(cliflags.AccessTokenFlag), + strings.ToUpper(op.HTTPMethod), + path, + contentType, + query, + jsonData, + ) if err != nil { - return err - } - - if res.StatusCode >= 400 { - err = errors.NewAPIError(body, nil, nil) - output, err := output.CmdOutputSingular( + out, err := output.CmdOutputSingular( viper.GetString(cliflags.OutputFlag), - body, + res, output.ErrorPlaintextOutputFn, ) if err != nil { return errors.NewError(err.Error()) } - return errors.NewError(output) - } - - output, err := output.CmdOutputSingular( - viper.GetString(cliflags.OutputFlag), - body, - op.PlaintextOutputFn, - ) - if err != nil { - return errors.NewError(err.Error()) + return errors.NewError(out) } - fmt.Fprintf(cmd.OutOrStdout(), output+"\n") + // todo: handle output + fmt.Fprintf(cmd.OutOrStdout(), string(res)+"\n") return nil } -func NewOperationCmd(parentCmd *cobra.Command, client *http.Client, op OperationData) *cobra.Command { +func NewOperationCmd(parentCmd *cobra.Command, client resources.Client, op OperationData) *cobra.Command { opCmd := OperationCmd{ OperationData: op, client: client, @@ -218,7 +199,6 @@ func NewOperationCmd(parentCmd *cobra.Command, client *http.Client, op Operation RunE: opCmd.makeRequest, Short: op.Short, Use: op.Use, - //TODO: add tracking here } opCmd.cmd = cmd diff --git a/cmd/root.go b/cmd/root.go index 2e922b8b..23aa27dd 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -3,10 +3,8 @@ package cmd import ( "fmt" "log" - "net/http" "os" "strings" - "time" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -17,12 +15,14 @@ import ( flagscmd "ldcli/cmd/flags" mbrscmd "ldcli/cmd/members" projcmd "ldcli/cmd/projects" + resourcecmd "ldcli/cmd/resources" "ldcli/internal/analytics" "ldcli/internal/config" "ldcli/internal/environments" "ldcli/internal/flags" "ldcli/internal/members" "ldcli/internal/projects" + "ldcli/internal/resources" ) type APIClients struct { @@ -30,7 +30,7 @@ type APIClients struct { FlagsClient flags.Client MembersClient members.Client ProjectsClient projects.Client - GenericClient *http.Client + ResourcesClient resources.Client } func NewRootCommand( @@ -144,7 +144,7 @@ func NewRootCommand( cmd.AddCommand(projectsCmd) cmd.AddCommand(NewQuickStartCmd(clients.EnvironmentsClient, clients.FlagsClient)) - addAllResourceCmds(cmd, clients.GenericClient, analyticsTracker) + resourcecmd.AddAllResourceCmds(cmd, clients.ResourcesClient, analyticsTracker) return cmd, nil } @@ -155,7 +155,7 @@ func Execute(analyticsTracker analytics.Tracker, version string) { FlagsClient: flags.NewClient(version), MembersClient: members.NewClient(version), ProjectsClient: projects.NewClient(version), - GenericClient: &http.Client{Timeout: time.Second * 3}, + ResourcesClient: resources.NewClient(version), } rootCmd, err := NewRootCommand( analyticsTracker, diff --git a/internal/resources/client.go b/internal/resources/client.go new file mode 100644 index 00000000..750e49df --- /dev/null +++ b/internal/resources/client.go @@ -0,0 +1,53 @@ +package resources + +import ( + "bytes" + "fmt" + "io" + "net/http" + "net/url" + "time" + + "ldcli/internal/errors" +) + +type Client interface { + MakeRequest(accessToken, method, path, contentType string, query url.Values, data []byte) ([]byte, error) +} + +type ResourcesClient struct { + cliVersion string +} + +var _ Client = ResourcesClient{} + +func NewClient(cliVersion string) ResourcesClient { + return ResourcesClient{cliVersion: cliVersion} +} + +func (c ResourcesClient) MakeRequest(accessToken, method, path, contentType string, query url.Values, data []byte) ([]byte, error) { + client := http.Client{Timeout: 3 * time.Second} + + req, _ := http.NewRequest(method, path, bytes.NewReader(data)) + req.Header.Add("Authorization", accessToken) + req.Header.Add("Content-type", contentType) + req.Header.Set("User-Agent", fmt.Sprintf("launchdarkly-cli/v%s", c.cliVersion)) + req.URL.RawQuery = query.Encode() + + res, err := client.Do(req) + if err != nil { + return nil, err + } + + body, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + defer res.Body.Close() + + if res.StatusCode >= 400 { + return body, errors.NewAPIError(body, nil, nil) + } + + return body, nil +} diff --git a/internal/resources/mock_client.go b/internal/resources/mock_client.go new file mode 100644 index 00000000..18d6395a --- /dev/null +++ b/internal/resources/mock_client.go @@ -0,0 +1 @@ +package resources From 0fd2c6b0e33df0bd86acc1c60355ecb9194a569a Mon Sep 17 00:00:00 2001 From: Kelly Hofmann Date: Mon, 29 Apr 2024 16:14:27 -0700 Subject: [PATCH 05/10] fix tests --- cmd/resources/resource_cmds_test.go | 41 +++++++++++++---------------- internal/resources/mock_client.go | 1 - 2 files changed, 18 insertions(+), 24 deletions(-) delete mode 100644 internal/resources/mock_client.go diff --git a/cmd/resources/resource_cmds_test.go b/cmd/resources/resource_cmds_test.go index b15847b2..291756e1 100644 --- a/cmd/resources/resource_cmds_test.go +++ b/cmd/resources/resource_cmds_test.go @@ -1,8 +1,6 @@ -package cmd_test +package resources_test import ( - "github.com/stretchr/testify/mock" - resources "ldcli/internal/resources" "testing" "github.com/stretchr/testify/assert" @@ -25,24 +23,21 @@ func TestCreateTeam(t *testing.T) { require.NoError(t, err) assert.Contains(t, string(output), "Create a team.") }) - t.Run("with valid flags calls makeRequest function", func(t *testing.T) { - client := resources.MockClient{} - client. - On("MakeRequest", mock.Anything). - Return() - args := []string{ - "teams", - "create", - "--access-token", - "abcd1234", - "--data", - `{"key": "team-key", "name": "Team Name"}`, - } - - output, err := cmd.CallCmd(t, cmd.APIClients{}, &analytics.NoopClient{}, args) - - require.NoError(t, err) - s := string(output) - assert.Contains(t, s, "would be making a post request to /api/v2/teams here, with args: map[data:map[key:team-key name:Team Name] expand:]\n") - }) + // TODO: add back test when mock client is added + //t.Run("with valid flags calls makeRequest function", func(t *testing.T) { + // args := []string{ + // "teams", + // "create", + // "--access-token", + // "abcd1234", + // "--data", + // `{"key": "team-key", "name": "Team Name"}`, + // } + // + // output, err := cmd.CallCmd(t, cmd.APIClients{}, &analytics.NoopClient{}, args) + // + // require.NoError(t, err) + // s := string(output) + // assert.Contains(t, s, "would be making a post request to /api/v2/teams here, with args: map[data:map[key:team-key name:Team Name] expand:]\n") + //}) } diff --git a/internal/resources/mock_client.go b/internal/resources/mock_client.go deleted file mode 100644 index 18d6395a..00000000 --- a/internal/resources/mock_client.go +++ /dev/null @@ -1 +0,0 @@ -package resources From 16349f9b9bb1008bc69179a94345d015321bf5b8 Mon Sep 17 00:00:00 2001 From: Kelly Hofmann Date: Mon, 29 Apr 2024 16:18:22 -0700 Subject: [PATCH 06/10] remove unused stuff --- cmd/resources/resources.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cmd/resources/resources.go b/cmd/resources/resources.go index 0fa94230..57834be6 100644 --- a/cmd/resources/resources.go +++ b/cmd/resources/resources.go @@ -48,7 +48,6 @@ type OperationData struct { RequiresBody bool Path string SupportsSemanticPatch bool // TBD on how to actually determine from openapi spec - PlaintextOutputFn output.PlaintextOutputFn } type Param struct { @@ -148,7 +147,7 @@ func (op *OperationCmd) makeRequest(cmd *cobra.Command, args []string) error { case "path": urlParms = append(urlParms, fmt.Sprintf("%v", val)) case "query": - query.Add(p.Name, fmt.Sprintf("%v", val)) + query.Add(p.Name, val) } } } From d2b53da6a10f45faf6fad5d26355e33f10c4de0d Mon Sep 17 00:00:00 2001 From: Kelly Hofmann Date: Mon, 29 Apr 2024 16:20:39 -0700 Subject: [PATCH 07/10] remove more stuff --- cmd/resources/resource_cmds.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/cmd/resources/resource_cmds.go b/cmd/resources/resource_cmds.go index c9fa6f38..e64901b5 100644 --- a/cmd/resources/resource_cmds.go +++ b/cmd/resources/resource_cmds.go @@ -6,7 +6,6 @@ import ( "github.com/spf13/cobra" "ldcli/internal/analytics" - "ldcli/internal/output" "ldcli/internal/resources" ) @@ -33,10 +32,9 @@ func AddAllResourceCmds(rootCmd *cobra.Command, client resources.Client, analyti Type: "string", }, }, - HTTPMethod: "post", - RequiresBody: true, - Path: "/api/v2/teams", - PlaintextOutputFn: output.SingularPlaintextOutputFn, + HTTPMethod: "post", + RequiresBody: true, + Path: "/api/v2/teams", }) NewOperationCmd(gen_TeamsResourceCmd, client, OperationData{ Short: "Get team", From f86822f9fe46fad7d4d56b9355afa9685bc0b0ac Mon Sep 17 00:00:00 2001 From: Kelly Hofmann Date: Wed, 1 May 2024 09:29:03 -0700 Subject: [PATCH 08/10] pr feedback --- cmd/resources/resource_cmds_test.go | 34 ++++++++++++++--------------- cmd/resources/resources.go | 19 +++++----------- 2 files changed, 22 insertions(+), 31 deletions(-) diff --git a/cmd/resources/resource_cmds_test.go b/cmd/resources/resource_cmds_test.go index 291756e1..464b567e 100644 --- a/cmd/resources/resource_cmds_test.go +++ b/cmd/resources/resource_cmds_test.go @@ -23,21 +23,21 @@ func TestCreateTeam(t *testing.T) { require.NoError(t, err) assert.Contains(t, string(output), "Create a team.") }) - // TODO: add back test when mock client is added - //t.Run("with valid flags calls makeRequest function", func(t *testing.T) { - // args := []string{ - // "teams", - // "create", - // "--access-token", - // "abcd1234", - // "--data", - // `{"key": "team-key", "name": "Team Name"}`, - // } - // - // output, err := cmd.CallCmd(t, cmd.APIClients{}, &analytics.NoopClient{}, args) - // - // require.NoError(t, err) - // s := string(output) - // assert.Contains(t, s, "would be making a post request to /api/v2/teams here, with args: map[data:map[key:team-key name:Team Name] expand:]\n") - //}) + t.Run("with valid flags calls makeRequest function", func(t *testing.T) { + t.Skip("TODO: add back when mock client is added") + args := []string{ + "teams", + "create", + "--access-token", + "abcd1234", + "--data", + `{"key": "team-key", "name": "Team Name"}`, + } + + output, err := cmd.CallCmd(t, cmd.APIClients{}, &analytics.NoopClient{}, args) + + require.NoError(t, err) + s := string(output) + assert.Contains(t, s, "would be making a post request to /api/v2/teams here, with args: map[data:map[key:team-key name:Team Name] expand:]\n") + }) } diff --git a/cmd/resources/resources.go b/cmd/resources/resources.go index 57834be6..6587fc61 100644 --- a/cmd/resources/resources.go +++ b/cmd/resources/resources.go @@ -112,7 +112,7 @@ func (op *OperationCmd) initFlags() error { return nil } -func formatURL(baseURI, path string, urlParams []string) string { +func buildURLWithParams(baseURI, path string, urlParams []string) string { s := make([]interface{}, len(urlParams)) for i, v := range urlParams { s[i] = v @@ -145,17 +145,17 @@ func (op *OperationCmd) makeRequest(cmd *cobra.Command, args []string) error { if val != "" { switch p.In { case "path": - urlParms = append(urlParms, fmt.Sprintf("%v", val)) + urlParms = append(urlParms, val) case "query": query.Add(p.Name, val) } } } - path := formatURL(viper.GetString(cliflags.BaseURIFlag), op.Path, urlParms) + path := buildURLWithParams(viper.GetString(cliflags.BaseURIFlag), op.Path, urlParms) contentType := "application/json" - if op.SupportsSemanticPatch && viper.GetBool("semantic-patch") { + if viper.GetBool("semantic-patch") { contentType += "; domain-model=launchdarkly.semanticpatch" } @@ -168,16 +168,7 @@ func (op *OperationCmd) makeRequest(cmd *cobra.Command, args []string) error { jsonData, ) if err != nil { - out, err := output.CmdOutputSingular( - viper.GetString(cliflags.OutputFlag), - res, - output.ErrorPlaintextOutputFn, - ) - if err != nil { - return errors.NewError(err.Error()) - } - - return errors.NewError(out) + return errors.NewError(output.CmdOutputError(viper.GetString(cliflags.OutputFlag), err)) } // todo: handle output From 3407c9c9a7944f98c6dda6f78fd17c3df0808ea7 Mon Sep 17 00:00:00 2001 From: Kelly Hofmann Date: Wed, 1 May 2024 09:31:26 -0700 Subject: [PATCH 09/10] fix output --- cmd/resources/resources.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cmd/resources/resources.go b/cmd/resources/resources.go index 6587fc61..330ca49d 100644 --- a/cmd/resources/resources.go +++ b/cmd/resources/resources.go @@ -171,8 +171,12 @@ func (op *OperationCmd) makeRequest(cmd *cobra.Command, args []string) error { return errors.NewError(output.CmdOutputError(viper.GetString(cliflags.OutputFlag), err)) } - // todo: handle output - fmt.Fprintf(cmd.OutOrStdout(), string(res)+"\n") + output, err := output.CmdOutput("get", viper.GetString(cliflags.OutputFlag), res) + if err != nil { + return errors.NewError(err.Error()) + } + + fmt.Fprintf(cmd.OutOrStdout(), output+"\n") return nil } From 862ae41ecb7836befe3dde2d9edc014b00201905 Mon Sep 17 00:00:00 2001 From: Kelly Hofmann Date: Wed, 1 May 2024 09:32:38 -0700 Subject: [PATCH 10/10] fix teams cmd help text --- cmd/resources/resource_cmds.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/resources/resource_cmds.go b/cmd/resources/resource_cmds.go index e64901b5..29e6a2f6 100644 --- a/cmd/resources/resource_cmds.go +++ b/cmd/resources/resource_cmds.go @@ -15,8 +15,8 @@ func AddAllResourceCmds(rootCmd *cobra.Command, client resources.Client, analyti rootCmd, analyticsTracker, "teams", - "A team is a group of members in your LaunchDarkly account.", - "A team can have maintainers who are able to add and remove team members. It also can have custom roles assigned to it that allows shared access to those roles for all team members. To learn more, read [Teams](https://docs.launchdarkly.com/home/teams).\n\nThe Teams API allows you to create, read, update, and delete a team.\n\nSeveral of the endpoints in the Teams API require one or more member IDs. The member ID is returned as part of the [List account members](/tag/Account-members#operation/getMembers) response. It is the `_id` field of each element in the `items` array.", + "Make requests (list, create, etc.) on teams", + "A team is a group of members in your LaunchDarkly account. A team can have maintainers who are able to add and remove team members. It also can have custom roles assigned to it that allows shared access to those roles for all team members. To learn more, read [Teams](https://docs.launchdarkly.com/home/teams).\n\nThe Teams API allows you to create, read, update, and delete a team.\n\nSeveral of the endpoints in the Teams API require one or more member IDs. The member ID is returned as part of the [List account members](/tag/Account-members#operation/getMembers) response. It is the `_id` field of each element in the `items` array.", ) // Operation commands