diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c9cc10b..1987a8e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -47,6 +47,10 @@ jobs: - name: Unit Tests timeout-minutes: 15 + env: + EPCC_CLIENT_ID: ${{ secrets.EPCC_CLIENT_ID }} + EPCC_CLIENT_SECRET: ${{ secrets.EPCC_CLIENT_SECRET }} + EPCC_API_BASE_URL: ${{ vars.EPCC_API_BASE_URL }} run: | go test -v -cover ./cmd/ ./external/... diff --git a/README.md b/README.md index 1fa9707..b4be32d 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Additionally, this tool is not necessarily meant to be a new command line equiva 2. Add the `epcc` binary to your path. 3. Load the autocompletion into your shell (See instructions [here](#completion)). -It is highly recommended that new users check out the [Tutorial](docs/tutorial.md) +It is highly recommended that new users check out the [Tutorial](docs/tutorial.md). ### Command Overview diff --git a/cmd/create.go b/cmd/create.go index c5bd612..d2de336 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -22,126 +22,150 @@ import ( func NewCreateCommand(parentCmd *cobra.Command) { - var autoFillOnCreate = false - - var outputJq = "" - overrides := &httpclient.HttpParameterOverrides{ - QueryParameters: nil, - OverrideUrlPath: "", + var create = &cobra.Command{ + Use: "create", + Short: "Creates a resource", + SilenceUsage: false, } - var create = &cobra.Command{ - Use: "create [ID_1] [ID_2]... ...", - Short: "Creates an entity of a resource.", - Args: cobra.MinimumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - body, err := createInternal(context.Background(), overrides, args, autoFillOnCreate) - - if err != nil { - return err - } + for _, resource := range resources.GetPluralResources() { + + var autoFillOnCreate = false + + var noBodyPrint = false + + var outputJq = "" + overrides := &httpclient.HttpParameterOverrides{ + QueryParameters: nil, + OverrideUrlPath: "", + } + + if resource.CreateEntityInfo == nil { + continue + } + + resource := resource + resourceName := resource.SingularName - if outputJq != "" { - output, err := json.RunJQOnStringWithArray(outputJq, body) + var createResourceCmd = &cobra.Command{ + Use: GetCreateUsageString(resource), + Short: GetCreateShort(resource), + Long: GetCreateLong(resource), + Example: GetCreateExample(resource), + Args: GetArgFunctionForCreate(resource), + RunE: func(cmd *cobra.Command, args []string) error { + body, err := createInternal(context.Background(), overrides, append([]string{resourceName}, args...), autoFillOnCreate) if err != nil { return err } - for _, outputLine := range output { - outputJson, err := gojson.Marshal(outputLine) + if outputJq != "" { + output, err := json.RunJQOnStringWithArray(outputJq, body) if err != nil { return err } - err = json.PrintJson(string(outputJson)) + for _, outputLine := range output { + outputJson, err := gojson.Marshal(outputLine) - if err != nil { - return err - } - } + if err != nil { + return err + } - return nil - } + err = json.PrintJson(string(outputJson)) - return json.PrintJson(body) - }, + if err != nil { + return err + } + } - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if len(args) == 0 { - return completion.Complete(completion.Request{ - Type: completion.CompleteSingularResource, - Verb: completion.Create, - }) - } + return nil + } - // Find Resource - resource, ok := resources.GetResourceByName(args[0]) - if ok { - if resource.CreateEntityInfo != nil { - resourceURL := resource.CreateEntityInfo.Url - idCount, _ := resources.GetNumberOfVariablesNeeded(resourceURL) - if len(args)-idCount >= 1 { // Arg is after IDs - if (len(args)-idCount)%2 == 1 { // This is an attribute key - usedAttributes := make(map[string]int) - for i := idCount + 1; i < len(args); i = i + 2 { - usedAttributes[args[i]] = 0 - } + if noBodyPrint { + return nil + } else { + return json.PrintJson(body) + } - // I think this allows you to complete the current argument - // This is necessary because if you are using something with a wildcard or regex - // You won't see it in the attribute list, and therefor it won't be able to auto complete it. - toComplete := strings.ReplaceAll(toComplete, "", "") - if toComplete != "" { - usedAttributes[toComplete] = 0 + }, + + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + // Find Resource + resource, ok := resources.GetResourceByName(resourceName) + if ok { + if resource.CreateEntityInfo != nil { + resourceURL := resource.CreateEntityInfo.Url + idCount, _ := resources.GetNumberOfVariablesNeeded(resourceURL) + if len(args)-idCount >= 0 { // Arg is after IDs + if (len(args)-idCount)%2 == 0 { // This is an attribute key + usedAttributes := make(map[string]int) + for i := idCount; i < len(args); i = i + 2 { + usedAttributes[args[i]] = 0 + } + + // I think this allows you to complete the current argument + // This is necessary because if you are using something with a wildcard or regex + // You won't see it in the attribute list, and therefore it won't be able to auto complete it. + // I now think this does nothing. + toComplete := strings.ReplaceAll(toComplete, "", "") + if toComplete != "" { + usedAttributes[toComplete] = 0 + } + return completion.Complete(completion.Request{ + Type: completion.CompleteAttributeKey, + Resource: resource, + Attributes: usedAttributes, + Verb: completion.Create, + ToComplete: toComplete, + }) + } else { // This is an attribute value + return completion.Complete(completion.Request{ + Type: completion.CompleteAttributeValue, + Resource: resource, + Verb: completion.Create, + Attribute: args[len(args)-1], + ToComplete: toComplete, + }) } - return completion.Complete(completion.Request{ - Type: completion.CompleteAttributeKey, - Resource: resource, - Attributes: usedAttributes, - Verb: completion.Create, - }) - } else { // This is an attribute value - return completion.Complete(completion.Request{ - Type: completion.CompleteAttributeValue, - Resource: resource, - Verb: completion.Create, - Attribute: args[len(args)-1], - ToComplete: toComplete, - }) - } - } else { - // Arg is in IDS - // Must be for a resource completion - types, err := resources.GetTypesOfVariablesNeeded(resourceURL) + } else { + // Arg is in IDS + // Must be for a resource completion + types, err := resources.GetTypesOfVariablesNeeded(resourceURL) - if err != nil { - return []string{}, cobra.ShellCompDirectiveNoFileComp - } + if err != nil { + return []string{}, cobra.ShellCompDirectiveNoFileComp + } - typeIdxNeeded := len(args) - 1 + typeIdxNeeded := len(args) - if completionResource, ok := resources.GetResourceByName(types[typeIdxNeeded]); ok { - return completion.Complete(completion.Request{ - Type: completion.CompleteAlias, - Resource: completionResource, - }) + if completionResource, ok := resources.GetResourceByName(types[typeIdxNeeded]); ok { + return completion.Complete(completion.Request{ + Type: completion.CompleteAlias, + Resource: completionResource, + }) + } } } } - } - return []string{}, cobra.ShellCompDirectiveNoFileComp - }, - } + return []string{}, cobra.ShellCompDirectiveNoFileComp + }, + } + + createResourceCmd.PersistentFlags().StringVar(&overrides.OverrideUrlPath, "override-url-path", "", "Override the URL that will be used for the Request") + createResourceCmd.PersistentFlags().BoolVarP(&autoFillOnCreate, "auto-fill", "", false, "Auto generate value for fields") + createResourceCmd.PersistentFlags().BoolVarP(&noBodyPrint, "silent", "s", false, "Don't print the body on success") + createResourceCmd.PersistentFlags().StringSliceVarP(&overrides.QueryParameters, "query-parameters", "q", []string{}, "Pass in key=value an they will be added as query parameters") + createResourceCmd.PersistentFlags().StringVarP(&outputJq, "output-jq", "", "", "A jq expression, if set we will restrict output to only this") - create.Flags().StringVar(&overrides.OverrideUrlPath, "override-url-path", "", "Override the URL that will be used for the Request") - create.Flags().BoolVarP(&autoFillOnCreate, "auto-fill", "", false, "Auto generate value for fields") - create.Flags().StringSliceVarP(&overrides.QueryParameters, "query-parameters", "q", []string{}, "Pass in key=value an they will be added as query parameters") - create.Flags().StringVarP(&outputJq, "output-jq", "", "", "A jq expression, if set we will restrict output to only this") + _ = createResourceCmd.RegisterFlagCompletionFunc("output-jq", jqCompletionFunc) + + create.AddCommand(createResourceCmd) + } - _ = create.RegisterFlagCompletionFunc("output-jq", jqCompletionFunc) parentCmd.AddCommand(create) } diff --git a/cmd/create_completion_test.go b/cmd/create_completion_test.go deleted file mode 100644 index 9a27957..0000000 --- a/cmd/create_completion_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package cmd - -import ( - "github.com/spf13/cobra" - "github.com/stretchr/testify/require" - "testing" -) - -func TestCreateCompletionReturnsSomeTypes(t *testing.T) { - - // Fixture Setup - rootCmd := &cobra.Command{} - NewCreateCommand(rootCmd) - create := rootCmd.Commands()[0] - - // Execute SUT - completionResult, _ := create.ValidArgsFunction(create, []string{}, "") - - // Verify - require.Contains(t, completionResult, "customer") - require.Contains(t, completionResult, "account") -} - -func TestCreateCompletionReturnsSomeFields(t *testing.T) { - - // Fixture Setup - rootCmd := &cobra.Command{} - NewCreateCommand(rootCmd) - create := rootCmd.Commands()[0] - - // Execute SUT - completionResult, _ := create.ValidArgsFunction(create, []string{"customer"}, "") - - // Verify - require.Contains(t, completionResult, "name") - require.Contains(t, completionResult, "email") -} - -func TestCreateCompletionReturnsSomeFieldWhileExcludingUsedOnes(t *testing.T) { - - // Fixture Setup - rootCmd := &cobra.Command{} - NewCreateCommand(rootCmd) - create := rootCmd.Commands()[0] - - // Execute SUT - completionResult, _ := create.ValidArgsFunction(create, []string{"customer", "name", "John"}, "") - - // Verify - - require.Contains(t, completionResult, "email") - require.NotContains(t, completionResult, "name") -} diff --git a/cmd/create_test.go b/cmd/create_test.go new file mode 100644 index 0000000..fbd0745 --- /dev/null +++ b/cmd/create_test.go @@ -0,0 +1,153 @@ +package cmd + +import ( + "github.com/elasticpath/epcc-cli/external/aliases" + "github.com/spf13/cobra" + "github.com/stretchr/testify/require" + "testing" +) + +func TestCreateCompletionReturnsSomeFields(t *testing.T) { + + // Fixture Setup + rootCmd := &cobra.Command{} + NewCreateCommand(rootCmd) + create := getCommandForResource(rootCmd.Commands()[0], "account") + + require.NotNil(t, create, "Create command for account should exist") + + // Execute SUT + completionResult, _ := create.ValidArgsFunction(create, []string{}, "") + + // Verify + require.Contains(t, completionResult, "name") + require.Contains(t, completionResult, "legal_name") +} + +func TestCreateCompletionReturnsSomeFieldWhileExcludingUsedOnes(t *testing.T) { + + // Fixture Setup + rootCmd := &cobra.Command{} + NewCreateCommand(rootCmd) + create := getCommandForResource(rootCmd.Commands()[0], "account") + + require.NotNil(t, create, "Create command for account should exist") + + // Execute SUT + completionResult, _ := create.ValidArgsFunction(create, []string{"name", "John"}, "") + + // Verify + require.Contains(t, completionResult, "legal_name") + require.Contains(t, completionResult, "registration_id") + require.NotContains(t, completionResult, "name") +} + +func TestCreateCompletionReturnsFirstElementParentId(t *testing.T) { + + // Fixture Setup + err := aliases.ClearAllAliases() + + require.NoError(t, err) + + aliases.SaveAliasesForResources( + // language=JSON + ` +{ + "data": { + "id": "123", + "type": "account", + "name": "John" + } +}`) + + rootCmd := &cobra.Command{} + NewCreateCommand(rootCmd) + createCmd := getCommandForResource(rootCmd.Commands()[0], "account-address") + + require.NotNil(t, createCmd, "Update command for account-addresses should exist") + + // Execute SUT + completionResult, _ := createCmd.ValidArgsFunction(createCmd, []string{}, "") + + // Verify + require.Contains(t, completionResult, "name=John") +} + +func TestCreateCompletionReturnsAnValidAttributeKey(t *testing.T) { + + // Fixture Setup + rootCmd := &cobra.Command{} + NewCreateCommand(rootCmd) + createCmd := getCommandForResource(rootCmd.Commands()[0], "account-address") + + require.NotNil(t, createCmd, "Update command for account-addresses should exist") + + // Execute SUT + completionResult, _ := createCmd.ValidArgsFunction(createCmd, []string{"name=John"}, "") + + // Verify + require.Contains(t, completionResult, "county") + require.Contains(t, completionResult, "city") +} + +func TestCreateCompletionReturnsAnValidAttributeKeyThatHasNotBeenUsed(t *testing.T) { + + // Fixture Setup + rootCmd := &cobra.Command{} + NewCreateCommand(rootCmd) + createCmd := getCommandForResource(rootCmd.Commands()[0], "account-address") + + require.NotNil(t, createCmd, "Update command for account-addresses should exist") + + // Execute SUT + completionResult, _ := createCmd.ValidArgsFunction(createCmd, []string{"name=John", "city", "Whitewood"}, "") + + // Verify + require.Contains(t, completionResult, "county") + require.NotContains(t, completionResult, "city") +} + +func TestCreateCompletionReturnsAValidAttributeValue(t *testing.T) { + // Fixture Setup + rootCmd := &cobra.Command{} + NewCreateCommand(rootCmd) + createCmd := getCommandForResource(rootCmd.Commands()[0], "integration") + + require.NotNil(t, createCmd, "Update command for integrations should exist") + + // Execute SUT + completionResult, _ := createCmd.ValidArgsFunction(createCmd, []string{"integration_type"}, "") + + // Verify + require.Contains(t, completionResult, "webhook") + require.Contains(t, completionResult, "aws_sqs") +} + +func TestCreateArgFunctionForEntityUrlHasNoErrorWithNoArgs(t *testing.T) { + // Fixture Setup + resourceName := "account" + + rootCmd := &cobra.Command{} + NewCreateCommand(rootCmd) + createCmd := getCommandForResource(rootCmd.Commands()[0], resourceName) + + // Execute SUT + err := createCmd.Args(createCmd, []string{}) + + // Verification + require.NoError(t, err) +} + +func TestCreateArgFunctionForEntityUrlWithParentIdHasErrorWithNoArgs(t *testing.T) { + // Fixture Setup + resourceName := "account-address" + + rootCmd := &cobra.Command{} + NewCreateCommand(rootCmd) + createCmd := getCommandForResource(rootCmd.Commands()[0], resourceName) + + // Execute SUT + err := createCmd.Args(createCmd, []string{}) + // Verification + require.ErrorContains(t, err, "ACCOUNT_ID must be specified") +} diff --git a/cmd/crud_smoke_test.go b/cmd/crud_smoke_test.go new file mode 100644 index 0000000..5408723 --- /dev/null +++ b/cmd/crud_smoke_test.go @@ -0,0 +1,85 @@ +package cmd + +import ( + "github.com/elasticpath/epcc-cli/external/httpclient" + "github.com/spf13/cobra" + "github.com/stretchr/testify/require" + "testing" +) + +func TestCrudOnAResource(t *testing.T) { + + httpclient.Initialize(1, 60) + + cmd := getTestCommand() + cmd.SetArgs([]string{"create", "account", "name", "Test", "legal_name", "Test", "--output-jq", ".data.id"}) + err := cmd.Execute() + require.NoError(t, err) + + cmd = getTestCommand() + cmd.SetArgs([]string{"get", "account", "name=Test", "--output-jq", ".data.name"}) + err = cmd.Execute() + require.NoError(t, err) + + cmd = getTestCommand() + cmd.SetArgs([]string{"get", "accounts", "--output-jq", ".data[].name"}) + err = cmd.Execute() + require.NoError(t, err) + + cmd = getTestCommand() + cmd.SetArgs([]string{"update", "account", "name=Test", "legal_name", "Test Update", "--output-jq", ".data.legal_name"}) + err = cmd.Execute() + require.NoError(t, err) + + cmd = getTestCommand() + cmd.SetArgs([]string{"delete", "account", "name=Test"}) + err = cmd.Execute() + require.NoError(t, err) + + // Error because this UUID doesn't exist + cmd = getTestCommand() + cmd.SetArgs([]string{"delete", "account", "6e7e2cdb-ff61-45a9-956b-c9dfc28d11d0"}) + err = cmd.Execute() + require.Error(t, err) + + // No error because of argument + cmd = getTestCommand() + cmd.SetArgs([]string{"delete", "account", "6e7e2cdb-ff61-45a9-956b-c9dfc28d11d0", "--allow-404"}) + err = cmd.Execute() + require.NoError(t, err) + + // Missing required arg + cmd = getTestCommand() + cmd.SetArgs([]string{"create", "account"}) + err = cmd.Execute() + require.Error(t, err) + + // Resource doesn't exist + cmd = getTestCommand() + cmd.SetArgs([]string{"update", "account", "6e7e2cdb-ff61-45a9-956b-c9dfc28d11d0"}) + err = cmd.Execute() + require.Error(t, err) + + // Resource doesn't exist + cmd = getTestCommand() + cmd.SetArgs([]string{"delete", "account", "6e7e2cdb-ff61-45a9-956b-c9dfc28d11d0"}) + err = cmd.Execute() + require.Error(t, err) + +} + +func getTestCommand() *cobra.Command { + testRootCmd := &cobra.Command{ + SilenceUsage: true, + } + + NewCreateCommand(testRootCmd) + NewGetCommand(testRootCmd) + NewUpdateCommand(testRootCmd) + NewDeleteCommand(testRootCmd) + + initConfig() + + return testRootCmd + +} diff --git a/cmd/delete-all.go b/cmd/delete-all.go index a7adc82..f41daf9 100644 --- a/cmd/delete-all.go +++ b/cmd/delete-all.go @@ -5,7 +5,6 @@ import ( "fmt" "github.com/elasticpath/epcc-cli/external/aliases" "github.com/elasticpath/epcc-cli/external/apihelper" - "github.com/elasticpath/epcc-cli/external/completion" "github.com/elasticpath/epcc-cli/external/httpclient" "github.com/elasticpath/epcc-cli/external/id" "github.com/elasticpath/epcc-cli/external/resources" @@ -21,26 +20,31 @@ import ( func NewDeleteAllCommand(parentCmd *cobra.Command) { var deleteAll = &cobra.Command{ - Use: "delete-all [RESOURCE]", - Short: "Deletes all of a resource.", - Args: cobra.MinimumNArgs(1), - Hidden: false, - RunE: func(cmd *cobra.Command, args []string) error { - return deleteAllInternal(context.Background(), args) - }, - - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if len(args) == 0 { - return completion.Complete(completion.Request{ - Type: completion.CompletePluralResource, - Verb: completion.DeleteAll, - }) - } - - return []string{}, cobra.ShellCompDirectiveNoFileComp - }, + Use: "delete-all", + Short: "Deletes all of a resource", + SilenceUsage: false, } + for _, resource := range resources.GetPluralResources() { + if resource.GetCollectionInfo == nil { + continue + } + + if resource.DeleteEntityInfo == nil { + continue + } + resourceName := resource.PluralName + + var deleteAllResourceCmd = &cobra.Command{ + Use: resourceName, + Short: GetDeleteAllShort(resource), + Hidden: false, + RunE: func(cmd *cobra.Command, args []string) error { + return deleteAllInternal(context.Background(), append([]string{resourceName}, args...)) + }, + } + deleteAll.AddCommand(deleteAllResourceCmd) + } parentCmd.AddCommand(deleteAll) } diff --git a/cmd/delete-all_test.go b/cmd/delete-all_test.go new file mode 100644 index 0000000..1d619dd --- /dev/null +++ b/cmd/delete-all_test.go @@ -0,0 +1 @@ +package cmd diff --git a/cmd/delete.go b/cmd/delete.go index 2e810ed..47bcf14 100644 --- a/cmd/delete.go +++ b/cmd/delete.go @@ -19,33 +19,48 @@ import ( func NewDeleteCommand(parentCmd *cobra.Command) { - overrides := &httpclient.HttpParameterOverrides{ - QueryParameters: nil, - OverrideUrlPath: "", + var deleteCmd = &cobra.Command{ + Use: "delete", + Short: "Deletes a resource", + SilenceUsage: false, } - var delete = &cobra.Command{ - Use: "delete [RESOURCE] [ID_1] [ID_2]", - Short: "Deletes a single resource.", - Args: cobra.MinimumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { + for _, resource := range resources.GetPluralResources() { + if resource.DeleteEntityInfo == nil { + continue + } + overrides := &httpclient.HttpParameterOverrides{ + QueryParameters: nil, + OverrideUrlPath: "", + } - body, err := deleteInternal(context.Background(), overrides, args) - if err != nil { - return err - } + var allow404 = false + + resource := resource + resourceName := resource.SingularName + + var deleteResourceCommand = &cobra.Command{ + Use: GetDeleteUsage(resource), + Short: GetDeleteShort(resource), + Long: GetDeleteLong(resource), + Example: GetDeleteExample(resource), + Args: GetArgFunctionForDelete(resource), + RunE: func(cmd *cobra.Command, args []string) error { + + body, err := deleteInternal(context.Background(), overrides, allow404, append([]string{resourceName}, args...)) + + if err != nil { + if body != "" { + json.PrintJson(body) + } + return err + } + + return json.PrintJson(body) + }, + + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return json.PrintJson(body) - }, - - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if len(args) == 0 { - return completion.Complete(completion.Request{ - Type: completion.CompleteSingularResource, - Verb: completion.Delete, - }) - } else if resource, ok := resources.GetResourceByName(args[0]); ok { - // len(args) == 0 means complete resource // len(args) == 1 means first id // lens(args) == 2 means second id. @@ -60,7 +75,7 @@ func NewDeleteCommand(parentCmd *cobra.Command) { return []string{}, cobra.ShellCompDirectiveNoFileComp } - if len(args) > 0 && len(args) < 1+idCount { + if len(args) < idCount { // Must be for a resource completion types, err := resources.GetTypesOfVariablesNeeded(resource.DeleteEntityInfo.Url) @@ -68,7 +83,7 @@ func NewDeleteCommand(parentCmd *cobra.Command) { return []string{}, cobra.ShellCompDirectiveNoFileComp } - typeIdxNeeded := len(args) - 1 + typeIdxNeeded := len(args) if completionResource, ok := resources.GetResourceByName(types[typeIdxNeeded]); ok { return completion.Complete(completion.Request{ @@ -77,9 +92,9 @@ func NewDeleteCommand(parentCmd *cobra.Command) { }) } } else { - if (len(args)-idCount)%2 == 1 { // This is an attribute key + if (len(args)-idCount)%2 == 0 { // This is an attribute key usedAttributes := make(map[string]int) - for i := idCount + 1; i < len(args); i = i + 2 { + for i := idCount; i < len(args); i = i + 2 { usedAttributes[args[i]] = 0 } return completion.Complete(completion.Request{ @@ -98,18 +113,19 @@ func NewDeleteCommand(parentCmd *cobra.Command) { }) } } - } - return []string{}, cobra.ShellCompDirectiveNoFileComp - }, + return []string{}, cobra.ShellCompDirectiveNoFileComp + }, + } + deleteResourceCommand.Flags().StringVar(&overrides.OverrideUrlPath, "override-url-path", "", "Override the URL that will be used for the Request") + deleteResourceCommand.Flags().StringSliceVarP(&overrides.QueryParameters, "query-parameters", "q", []string{}, "Pass in key=value an they will be added as query parameters") + deleteResourceCommand.Flags().BoolVar(&allow404, "allow-404", allow404, "If set 404's will not be treated as errors") + deleteCmd.AddCommand(deleteResourceCommand) } - delete.Flags().StringVar(&overrides.OverrideUrlPath, "override-url-path", "", "Override the URL that will be used for the Request") - delete.Flags().StringSliceVarP(&overrides.QueryParameters, "query-parameters", "q", []string{}, "Pass in key=value an they will be added as query parameters") - - parentCmd.AddCommand(delete) + parentCmd.AddCommand(deleteCmd) } -func deleteInternal(ctx context.Context, overrides *httpclient.HttpParameterOverrides, args []string) (string, error) { +func deleteInternal(ctx context.Context, overrides *httpclient.HttpParameterOverrides, allow404 bool, args []string) (string, error) { crud.OutstandingRequestCounter.Add(1) defer crud.OutstandingRequestCounter.Done() @@ -141,6 +157,13 @@ func deleteInternal(ctx context.Context, overrides *httpclient.HttpParameterOver log.Fatal(err) } + // Check if error response + if resp.StatusCode >= 400 && resp.StatusCode <= 600 { + if resp.StatusCode != 404 || !allow404 { + return string(body), fmt.Errorf(resp.Status) + } + } + return string(body), nil } else { return "", nil diff --git a/cmd/delete_test.go b/cmd/delete_test.go new file mode 100644 index 0000000..9a42ac5 --- /dev/null +++ b/cmd/delete_test.go @@ -0,0 +1,196 @@ +package cmd + +import ( + "github.com/elasticpath/epcc-cli/external/aliases" + "github.com/spf13/cobra" + "github.com/stretchr/testify/require" + "testing" +) + +func TestDeleteCompletionReturnsFirstElementParentId(t *testing.T) { + // Fixture Setup + err := aliases.ClearAllAliases() + + require.NoError(t, err) + + aliases.SaveAliasesForResources( + // language=JSON + ` +{ + "data": { + "id": "123", + "type": "account", + "name": "John" + } +}`) + + rootCmd := &cobra.Command{} + NewDeleteCommand(rootCmd) + deleteCmd := getCommandForResource(rootCmd.Commands()[0], "account-address") + + require.NotNil(t, deleteCmd, "Delete command for account-addresses should exist") + + // Execute SUT + completionResult, _ := deleteCmd.ValidArgsFunction(deleteCmd, []string{}, "") + + // Verify + require.Contains(t, completionResult, "name=John") +} + +func TestDeleteCompletionReturnsSecondElementId(t *testing.T) { + // Fixture Setup + err := aliases.ClearAllAliases() + + require.NoError(t, err) + + aliases.SaveAliasesForResources( + // language=JSON + ` +{ + "data": { + "id": "123", + "type": "address" + } +}`) + + rootCmd := &cobra.Command{} + NewDeleteCommand(rootCmd) + deleteCmd := getCommandForResource(rootCmd.Commands()[0], "account-address") + + require.NotNil(t, deleteCmd, "Delete command for account-addresses should exist") + + // Execute SUT + completionResult, _ := deleteCmd.ValidArgsFunction(deleteCmd, []string{"name=John"}, "") + + // Verify + require.Contains(t, completionResult, "id=123") +} + +func TestDeleteCompletionReturnsAnValidAttributeKey(t *testing.T) { + + // Fixture Setup + rootCmd := &cobra.Command{} + NewDeleteCommand(rootCmd) + deleteCmd := getCommandForResource(rootCmd.Commands()[0], "account-address") + + require.NotNil(t, deleteCmd, "Delete command for account-addresses should exist") + + // Execute SUT + completionResult, _ := deleteCmd.ValidArgsFunction(deleteCmd, []string{"name=John", "id=123"}, "") + + // Verify + require.Contains(t, completionResult, "county") + require.Contains(t, completionResult, "city") +} + +func TestDeleteCompletionReturnsAnValidAttributeKeyThatHasNotBeenUsed(t *testing.T) { + + // Fixture Setup + rootCmd := &cobra.Command{} + NewDeleteCommand(rootCmd) + deleteCmd := getCommandForResource(rootCmd.Commands()[0], "account-address") + + require.NotNil(t, deleteCmd, "Delete command for account-addresses should exist") + + // Execute SUT + completionResult, _ := deleteCmd.ValidArgsFunction(deleteCmd, []string{"name=John", "id=123", "city", "Lumsden"}, "") + + // Verify + require.Contains(t, completionResult, "county") + require.NotContains(t, completionResult, "city") +} + +func TestDeleteCompletionReturnsAnValidAttributeValue(t *testing.T) { + + // Fixture Setup + rootCmd := &cobra.Command{} + NewDeleteCommand(rootCmd) + deleteCmd := getCommandForResource(rootCmd.Commands()[0], "integration") + + require.NotNil(t, deleteCmd, "Delete command for account-addresses should exist") + + // Execute SUT + completionResult, _ := deleteCmd.ValidArgsFunction(deleteCmd, []string{"id=123", "integration_type"}, "") + + // Verify + require.Contains(t, completionResult, "webhook") + require.Contains(t, completionResult, "aws_sqs") +} + +func TestDeleteArgFunctionForEntityUrlHasErrorWithNoArgs(t *testing.T) { + // Fixture Setup + resourceName := "account" + + rootCmd := &cobra.Command{} + NewDeleteCommand(rootCmd) + deleteCmd := getCommandForResource(rootCmd.Commands()[0], resourceName) + + // Execute SUT + err := deleteCmd.Args(deleteCmd, []string{}) + + // Verification + require.ErrorContains(t, err, "ACCOUNT_ID must be specified") +} + +func TestDeleteArgFunctionForEntityUrlWithParentIdHasErrorWithNoArgs(t *testing.T) { + // Fixture Setup + resourceName := "account-address" + + rootCmd := &cobra.Command{} + NewDeleteCommand(rootCmd) + deleteCmd := getCommandForResource(rootCmd.Commands()[0], resourceName) + + // Execute SUT + err := deleteCmd.Args(deleteCmd, []string{}) + + // Verification + require.ErrorContains(t, err, "ACCOUNT_ID, ACCOUNT_ADDRESS_ID must be specified") + +} + +func TestDeleteArgFunctionForEntityUrlHasNoErrorWithArgs(t *testing.T) { + // Fixture Setup + resourceName := "account" + + rootCmd := &cobra.Command{} + NewDeleteCommand(rootCmd) + deleteCmd := getCommandForResource(rootCmd.Commands()[0], resourceName) + + // Execute SUT + err := deleteCmd.Args(deleteCmd, []string{"foo"}) + + // Verification + require.NoError(t, err) + +} + +func TestDeleteArgFunctionForEntityUrlWithParentIdHasErrorWithOneArgOnly(t *testing.T) { + // Fixture Setup + resourceName := "account-address" + + rootCmd := &cobra.Command{} + NewDeleteCommand(rootCmd) + deleteCmd := getCommandForResource(rootCmd.Commands()[0], resourceName) + + // Execute SUT + err := deleteCmd.Args(deleteCmd, []string{"foo"}) + + // Verification + require.ErrorContains(t, err, "ACCOUNT_ADDRESS_ID must be specified") + require.NotContains(t, err.Error(), "ACCOUNT_ID must be specified") +} + +func TestDeleteArgFunctionForEntityUrlWithParentIdHasNoErrorWithArgs(t *testing.T) { + // Fixture Setup + resourceName := "account-address" + + rootCmd := &cobra.Command{} + NewDeleteCommand(rootCmd) + deleteCmd := getCommandForResource(rootCmd.Commands()[0], resourceName) + + // Execute SUT + err := deleteCmd.Args(deleteCmd, []string{"foo", "bar"}) + + // Verification + require.NoError(t, err) +} diff --git a/cmd/get.go b/cmd/get.go index a384988..ccffe7c 100644 --- a/cmd/get.go +++ b/cmd/get.go @@ -18,138 +18,172 @@ import ( "strings" ) -func NewGetCommand(parentCmd *cobra.Command) { +const singularResourceRequest = 0 +const collectionResourceRequest = 1 +func NewGetCommand(parentCmd *cobra.Command) { overrides := &httpclient.HttpParameterOverrides{ QueryParameters: nil, OverrideUrlPath: "", } var outputJq = "" + var noBodyPrint = false - var get = &cobra.Command{ - Use: "get [RESOURCE] [ID_1] [ID_2]", - Short: "Retrieves a single resource.", - Args: cobra.MinimumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - body, err := getInternal(context.Background(), overrides, args) - if err != nil { - return err - } + var getCmd = &cobra.Command{ + Use: "get", + Short: "Retrieves either a single or all resources", + SilenceUsage: false, + } + + for _, resource := range resources.GetPluralResources() { + resource := resource + + for i := 0; i < 2; i++ { + i := i + //usageString := "" + resourceName := "" + completionVerb := 0 + usageGetType := "" + var urlInfo *resources.CrudEntityInfo = nil + + switch i { + case singularResourceRequest: + if resource.GetCollectionInfo == nil { + continue + } + + resourceName = resource.PluralName + //usageString = resource.PluralName - if outputJq != "" { - output, err := json.RunJQOnStringWithArray(outputJq, body) + urlInfo = resource.GetCollectionInfo + completionVerb = completion.GetAll + usageGetType = "all (or a single page) of" - if err != nil { - return err + case collectionResourceRequest: + if resource.GetEntityInfo == nil { + continue } - for _, outputLine := range output { - outputJson, err := gojson.Marshal(outputLine) + //usageString = resource.SingularName + resourceName = resource.SingularName - if err != nil { - return err - } + urlInfo = resource.GetEntityInfo + completionVerb = completion.Get + usageGetType = "a single" + } - err = json.PrintJson(string(outputJson)) + resourceUrl := urlInfo.Url + newCmd := &cobra.Command{ + Use: GetGetUsageString(resourceName, resourceUrl, completionVerb, resource), + // The replace all is a hack for the moment the URL could be made nicer + Short: GetGetShort(resourceUrl), + Long: GetGetLong(resourceName, resourceUrl, usageGetType, completionVerb, urlInfo, resource), + Example: GetGetExample(resourceName, resourceUrl, usageGetType, completionVerb, urlInfo, resource), + Args: GetArgFunctionForUrl(resourceName, resourceUrl), + RunE: func(cmd *cobra.Command, args []string) error { + + body, err := getInternal(context.Background(), overrides, append([]string{resourceName}, args...)) if err != nil { return err } - } - return nil - } + if outputJq != "" { + output, err := json.RunJQOnStringWithArray(outputJq, body) - return json.PrintJson(body) - }, - - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if len(args) == 0 { - return completion.Complete(completion.Request{ - Type: completion.CompleteSingularResource | completion.CompletePluralResource, - Verb: completion.Get, - }) - } else if resource, ok := resources.GetResourceByName(args[0]); ok { - // len(args) == 0 means complete resource - // len(args) == 1 means first id - // lens(args) == 2 means second id. - - resourceURL, err := getUrl(resource, args) - if err != nil { - return []string{}, cobra.ShellCompDirectiveNoFileComp - } + if err != nil { + return err + } + for _, outputLine := range output { + outputJson, err := gojson.Marshal(outputLine) - idCount, err := resources.GetNumberOfVariablesNeeded(resourceURL.Url) + if err != nil { + return err + } - if err != nil { - return []string{}, cobra.ShellCompDirectiveNoFileComp - } + err = json.PrintJson(string(outputJson)) - if len(args) > 0 && len(args) < 1+idCount { - // Must be for a resource completion - types, err := resources.GetTypesOfVariablesNeeded(resourceURL.Url) + if err != nil { + return err + } + } - if err != nil { + return nil + } + + if noBodyPrint { + return nil + } else { + return json.PrintJson(body) + } + + }, + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + + if resourceUrl == "" { return []string{}, cobra.ShellCompDirectiveNoFileComp } - typeIdxNeeded := len(args) - 1 + idCount, err := resources.GetNumberOfVariablesNeeded(resourceUrl) - if completionResource, ok := resources.GetResourceByName(types[typeIdxNeeded]); ok { - return completion.Complete(completion.Request{ - Type: completion.CompleteAlias, - Resource: completionResource, - }) + if err != nil { + return []string{}, cobra.ShellCompDirectiveNoFileComp } - } else if len(args) >= idCount+1 { // Arg is after IDs - if (len(args)-idCount)%2 == 1 { // This is a query param key - if resource.SingularName != args[0] { // If the resource is plural/get-collection + if len(args) < idCount { + // Must be for a resource completion + types, err := resources.GetTypesOfVariablesNeeded(resourceUrl) + + if err != nil { + return []string{}, cobra.ShellCompDirectiveNoFileComp + } + + typeIdxNeeded := len(args) + + if completionResource, ok := resources.GetResourceByName(types[typeIdxNeeded]); ok { return completion.Complete(completion.Request{ - Type: completion.CompleteQueryParamKey, - Resource: resource, - Verb: completion.GetAll, + Type: completion.CompleteAlias, + Resource: completionResource, }) - } else { + } + + } else if len(args) >= idCount { // Arg is after IDs + if (len(args)-idCount)%2 == 0 { // This is a query param key return completion.Complete(completion.Request{ Type: completion.CompleteQueryParamKey, Resource: resource, - Verb: completion.Get, - }) - } - } else { - // This is a query param value - if resource.SingularName != args[0] { // If the resource is plural/get-collection - return completion.Complete(completion.Request{ - Type: completion.CompleteQueryParamValue, - Resource: resource, - Verb: completion.GetAll, - QueryParam: args[len(args)-1], - ToComplete: toComplete, + Verb: completionVerb, }) + } else { return completion.Complete(completion.Request{ Type: completion.CompleteQueryParamValue, Resource: resource, - Verb: completion.Get, + Verb: completionVerb, QueryParam: args[len(args)-1], ToComplete: toComplete, }) + } } - } + + return []string{}, cobra.ShellCompDirectiveNoFileComp + }, } - return []string{}, cobra.ShellCompDirectiveNoFileComp - }, + newCmd.PersistentFlags().BoolVarP(&noBodyPrint, "silent", "s", false, "Don't print the body on success") + newCmd.PersistentFlags().StringVar(&overrides.OverrideUrlPath, "override-url-path", "", "Override the URL that will be used for the Request") + newCmd.PersistentFlags().StringSliceVarP(&overrides.QueryParameters, "query-parameters", "q", []string{}, "Pass in key=value an they will be added as query parameters") + newCmd.PersistentFlags().StringVarP(&outputJq, "output-jq", "", "", "A jq expression, if set we will restrict output to only this") + _ = newCmd.RegisterFlagCompletionFunc("output-jq", jqCompletionFunc) + + getCmd.AddCommand(newCmd) + } } - get.Flags().StringVar(&overrides.OverrideUrlPath, "override-url-path", "", "Override the URL that will be used for the Request") - get.Flags().StringSliceVarP(&overrides.QueryParameters, "query-parameters", "q", []string{}, "Pass in key=value an they will be added as query parameters") - get.Flags().StringVarP(&outputJq, "output-jq", "", "", "A jq expression, if set we will restrict output to only this") - _ = get.RegisterFlagCompletionFunc("output-jq", jqCompletionFunc) + _ = getCmd.RegisterFlagCompletionFunc("output-jq", jqCompletionFunc) - parentCmd.AddCommand(get) + parentCmd.AddCommand(getCmd) } func getInternal(ctx context.Context, overrides *httpclient.HttpParameterOverrides, args []string) (string, error) { diff --git a/cmd/get_test.go b/cmd/get_test.go new file mode 100644 index 0000000..cd4dc33 --- /dev/null +++ b/cmd/get_test.go @@ -0,0 +1,193 @@ +package cmd + +import ( + "github.com/elasticpath/epcc-cli/external/aliases" + "github.com/spf13/cobra" + "github.com/stretchr/testify/require" + "testing" +) + +func init() { + aliases.InitializeAliasDirectoryForTesting() +} + +func TestGetCompletionForCollectionResourceReturnsStandardFields(t *testing.T) { + + // Fixture Setup + rootCmd := &cobra.Command{} + NewGetCommand(rootCmd) + getCmd := getCommandForResource(rootCmd.Commands()[0], "accounts") + + require.NotNil(t, getCmd, "Get command for account should exist") + + // Execute SUT + completionResult, _ := getCmd.ValidArgsFunction(getCmd, []string{}, "") + + // Verify + require.Contains(t, completionResult, "sort") + require.Contains(t, completionResult, "include") + require.Contains(t, completionResult, "page[limit]") + require.Contains(t, completionResult, "page[offset]") + require.Contains(t, completionResult, "filter") +} + +func TestGetCompletionForCollectionResourceReturnsValuesForStandardField(t *testing.T) { + + // Fixture Setup + rootCmd := &cobra.Command{} + NewGetCommand(rootCmd) + getCmd := getCommandForResource(rootCmd.Commands()[0], "accounts") + + require.NotNil(t, getCmd, "Get command for account should exist") + + // Execute SUT + completionResult, _ := getCmd.ValidArgsFunction(getCmd, []string{"sort"}, "") + + // Verify + require.Contains(t, completionResult, "created_at") + +} + +func TestGetCompletionForCollectionWithParentResourceReturnsAlias(t *testing.T) { + + // Fixture Setup + err := aliases.ClearAllAliases() + + require.NoError(t, err) + + aliases.SaveAliasesForResources( + // language=JSON + ` +{ + "data": { + "id": "123", + "type": "account", + "name": "John" + } +}`) + + rootCmd := &cobra.Command{} + NewGetCommand(rootCmd) + getCmd := getCommandForResource(rootCmd.Commands()[0], "account-addresses") + + require.NotNil(t, getCmd, "Get command for account should exist") + + // Execute SUT + completionResult, _ := getCmd.ValidArgsFunction(getCmd, []string{}, "") + + // Verify + require.Contains(t, completionResult, "name=John") + +} + +func TestGetCompletionForCollectionWithParentResourceReturnsStandardFields(t *testing.T) { + + // Fixture Setup + rootCmd := &cobra.Command{} + NewGetCommand(rootCmd) + getCmd := getCommandForResource(rootCmd.Commands()[0], "account-addresses") + + require.NotNil(t, getCmd, "Get command for account should exist") + + // Execute SUT + completionResult, _ := getCmd.ValidArgsFunction(getCmd, []string{"foo"}, "") + + // Verify + require.Contains(t, completionResult, "sort") + require.Contains(t, completionResult, "include") + require.Contains(t, completionResult, "page[limit]") + require.Contains(t, completionResult, "page[offset]") + require.Contains(t, completionResult, "filter") +} + +func TestGetCompletionForCollectionResourceWithParentReturnsValuesForStandardField(t *testing.T) { + + // Fixture Setup + rootCmd := &cobra.Command{} + NewGetCommand(rootCmd) + getCmd := getCommandForResource(rootCmd.Commands()[0], "account-addresses") + + require.NotNil(t, getCmd, "Get command for account should exist") + + // Execute SUT + completionResult, _ := getCmd.ValidArgsFunction(getCmd, []string{"foo", "sort"}, "") + + // Verify + require.Contains(t, completionResult, "created_at") + +} + +func TestGetArgFunctionForCollectionUrlWithNoParentsHasNoErrorWithNoArgs(t *testing.T) { + // Fixture Setup + resourceName := "accounts" + + rootCmd := &cobra.Command{} + NewGetCommand(rootCmd) + getCmd := getCommandForResource(rootCmd.Commands()[0], resourceName) + + // Execute SUT + err := getCmd.Args(getCmd, []string{}) + + // Verification + require.NoError(t, err) +} + +func TestGetArgFunctionForCollectionUrlWithParentHasErrorWithNoArgs(t *testing.T) { + // Fixture Setup + resourceName := "account-addresses" + + rootCmd := &cobra.Command{} + NewGetCommand(rootCmd) + getCmd := getCommandForResource(rootCmd.Commands()[0], resourceName) + + // Execute SUT + err := getCmd.Args(getCmd, []string{}) + + // Verification + require.ErrorContains(t, err, "ACCOUNT_ID must be specified") +} + +func TestGetArgFunctionForEntityUrlHasErrorWithNoArgs(t *testing.T) { + // Fixture Setup + resourceName := "account" + + rootCmd := &cobra.Command{} + NewGetCommand(rootCmd) + getCmd := getCommandForResource(rootCmd.Commands()[0], resourceName) + + // Execute SUT + err := getCmd.Args(getCmd, []string{}) + + // Verification + require.ErrorContains(t, err, "ACCOUNT_ID must be specified") +} + +func TestGetArgFunctionForCollectionUrlWithParentHasNoErrorWithArgs(t *testing.T) { + // Fixture Setup + resourceName := "account-addresses" + + rootCmd := &cobra.Command{} + NewGetCommand(rootCmd) + getCmd := getCommandForResource(rootCmd.Commands()[0], resourceName) + + // Execute SUT + err := getCmd.Args(getCmd, []string{"foo"}) + + // Verification + require.NoError(t, err) +} + +func TestGetArgFunctionForEntityUrlHasNoErrorWithArgs(t *testing.T) { + // Fixture Setup + resourceName := "account" + + rootCmd := &cobra.Command{} + NewGetCommand(rootCmd) + getCmd := getCommandForResource(rootCmd.Commands()[0], resourceName) + + // Execute SUT + err := getCmd.Args(getCmd, []string{"foo"}) + + // Verification + require.NoError(t, err) +} diff --git a/cmd/helper.go b/cmd/helper.go new file mode 100644 index 0000000..da205a4 --- /dev/null +++ b/cmd/helper.go @@ -0,0 +1,830 @@ +package cmd + +import ( + "fmt" + "github.com/elasticpath/epcc-cli/external/autofill" + "github.com/elasticpath/epcc-cli/external/completion" + "github.com/elasticpath/epcc-cli/external/json" + "github.com/elasticpath/epcc-cli/external/resources" + "github.com/google/uuid" + "github.com/iancoleman/strcase" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/yosida95/uritemplate/v3" + "math/rand" + "net/url" + "regexp" + "strings" + "sync" +) + +func GetSingularTypeNames(types []string) []string { + var ret []string + + for _, t := range types { + + otherType, ok := resources.GetResourceByName(t) + + if !ok { + log.Warnf("Error processing resource, could not find type %s", t) + } + + ret = append(ret, otherType.SingularName) + } + + return ret +} + +func ConvertSingularTypeToCmdArg(typeName string) string { + return fmt.Sprintf("%s_ID", strings.ReplaceAll(strings.ToUpper(typeName), "-", "_")) +} +func GetParametersForTypes(types []string) string { + r := "" + + for _, t := range types { + r += " " + ConvertSingularTypeToCmdArg(t) + + } + + return r +} + +func GetParameterUsageForTypes(types []string) string { + r := "" + + for _, t := range types { + r += fmt.Sprintf("%-20s - An ID or alias for a %s\n", t, strings.Title(t)) + } + + return r +} + +func GetUuidsForTypes(types []string) []string { + r := []string{} + + for i := 0; i < len(types); i++ { + r = append(r, uuid.New().String()) + } + + return r +} + +func GetArgumentExampleWithIds(types []string, uuids []string) string { + r := "" + + for i := 0; i < len(types); i++ { + r += uuids[i] + } + + return r +} + +func GetArgumentExampleWithAlias(types []string) string { + r := "" + + for i := 0; i < len(types); i++ { + r += "last_read=entity " + } + + return r +} + +func GetHelpResourceUrls(resourceUrl string) string { + + template, err := uritemplate.New(resourceUrl) + + if err != nil { + return fmt.Sprintf("error: %s", err) + } + + values := uritemplate.Values{} + + for _, varName := range template.Varnames() { + res, ok := resources.GetResourceByName(resources.ConvertUriTemplateValueToType(varName)) + + if !ok { + values[varName] = uritemplate.String("unknown_resource:" + varName) + continue + } + + typeName := res.SingularName + typeName = strings.ReplaceAll(typeName, "-", " ") + typeName = strings.Title(typeName) + typeName = strings.ReplaceAll(typeName, " ", "") + typeName = strings.ReplaceAll(typeName, "V2", "") + typeName = strcase.ToLowerCamel(typeName) + + values[varName] = uritemplate.String(":" + typeName + "Id") + + } + + templateUrl, err := template.Expand(values) + + templateUrl, _ = url.PathUnescape(templateUrl) + + return templateUrl +} + +func GetArgFunctionForCreate(resource resources.Resource) func(cmd *cobra.Command, args []string) error { + return GetArgFunctionForUrl(resource.SingularName, resource.CreateEntityInfo.Url) +} + +func GetArgFunctionForUpdate(resource resources.Resource) func(cmd *cobra.Command, args []string) error { + return GetArgFunctionForUrl(resource.SingularName, resource.UpdateEntityInfo.Url) +} + +func GetArgFunctionForDelete(resource resources.Resource) func(cmd *cobra.Command, args []string) error { + return GetArgFunctionForUrl(resource.SingularName, resource.DeleteEntityInfo.Url) +} + +func GetArgFunctionForUrl(name, resourceUrl string) func(cmd *cobra.Command, args []string) error { + + singularTypeNames, err := resources.GetSingularTypesOfVariablesNeeded(resourceUrl) + + if err != nil { + log.Warnf("Could not generate usage string for %s, error %v", name, err) + } + + return func(cmd *cobra.Command, args []string) error { + var missingArgs []string + + for i, neededType := range singularTypeNames { + if len(args) < i+1 { + missingArgs = append(missingArgs, ConvertSingularTypeToCmdArg(neededType)) + + } + } + + if len(missingArgs) > 0 { + return fmt.Errorf("missing required arguments: %s must be specified, please see --help for more info", strings.Join(missingArgs, ", ")) + } else { + return nil + } + } +} + +var NonAlphaCharacter = regexp.MustCompile("[^A-Za-z]+") + +func GetJsonKeyValuesForUsage(resource resources.Resource) string { + var ret = "" + for k := range resource.Attributes { + + jsonKey := k + // A good example of why these are needed are pcm-products and the regex attributes + jsonKey = strings.ReplaceAll(jsonKey, "^", "") + jsonKey = strings.ReplaceAll(jsonKey, "$", "") + jsonKey = strings.ReplaceAll(jsonKey, "\\.", ".") + jsonKey = strings.ReplaceAll(jsonKey, "\\", "") + + jsonKey = strings.ReplaceAll(jsonKey, "([a-zA-Z0-9-_]+)", "*") + value := strings.Trim(NonAlphaCharacter.ReplaceAllString(strings.ToUpper(k), "_"), "_ ") + value = strings.ReplaceAll(value, "A_Z", "") + value = strings.ReplaceAll(value, "__", "_") + ret += " [" + jsonKey + " " + value + "]" + } + + return ret +} + +func GetJsonExample(description string, call string, header string, jsonTxt string) string { + + jsonTxt = "> " + json.PrettyPrint(jsonTxt) + jsonTxt = strings.ReplaceAll(jsonTxt, "\n", "\n > ") + + return fmt.Sprintf(` + %s + %s + %s + %s +`, description, call, header, jsonTxt) +} + +func FillUrlWithIds(urlInfo *resources.CrudEntityInfo, uuids []string) string { + var ids []string + + idsNeeded, err := resources.GetNumberOfVariablesNeeded(urlInfo.Url) + + if err != nil { + log.Errorf("error generating help screen %v", err) + } + + for i := 0; i < idsNeeded; i++ { + ids = append(ids, uuids[i]) + } + + url, err := resources.GenerateUrl(urlInfo, ids) + + if err != nil { + log.Errorf("error generating help screen %v", err) + } + + return url +} + +func GetGetShort(resourceUrl string) string { + return fmt.Sprintf("Calls GET %s", GetHelpResourceUrls(resourceUrl)) +} +func GetCreateShort(resource resources.Resource) string { + return fmt.Sprintf("Calls POST %s", GetHelpResourceUrls(resource.CreateEntityInfo.Url)) +} + +func GetUpdateShort(resource resources.Resource) string { + return fmt.Sprintf("Calls PUT %s", GetHelpResourceUrls(resource.UpdateEntityInfo.Url)) +} + +func GetDeleteShort(resource resources.Resource) string { + return fmt.Sprintf("Calls DELETE %s", GetHelpResourceUrls(resource.DeleteEntityInfo.Url)) +} + +func GetDeleteAllShort(resource resources.Resource) string { + return fmt.Sprintf("Calls DELETE %s for every resource in GET %s", GetHelpResourceUrls(resource.DeleteEntityInfo.Url), GetHelpResourceUrls(resource.GetCollectionInfo.Url)) +} + +func GetGetLong(resourceName string, resourceUrl string, usageGetType string, completionVerb int, urlInfo *resources.CrudEntityInfo, resource resources.Resource) string { + + types, err := resources.GetTypesOfVariablesNeeded(resourceUrl) + + if err != nil { + return fmt.Sprintf("Could not generate usage string: %s", err) + } + + singularTypeNames := GetSingularTypeNames(types) + parametersLongUsage := GetParameterUsageForTypes(singularTypeNames) + + return fmt.Sprintf(`Retrieves %s %s defined in a store/organization by calling %s. + +%s +`, usageGetType, resourceName, GetHelpResourceUrls(resourceUrl), parametersLongUsage) +} + +func GetJsonSyntaxExample(resource resources.Resource) string { + return fmt.Sprintf(` +Key and value pairs passed in will be converted to JSON with a jq like syntax. + +The EPCC CLI will automatically determine appropriate wrapping + +key b => %s +key 1 => %s +key '"1"' => %s +key true => %s +key null => %s +key '"null"'' => %s +key[0] a key[1] true => %s +key.some.child hello key.some.other goodbye => %s +`, + toJsonExample([]string{"key", "b"}, resource), + toJsonExample([]string{"key", "1"}, resource), + toJsonExample([]string{"key", "\"1\""}, resource), + toJsonExample([]string{"key", "true"}, resource), + toJsonExample([]string{"key", "null"}, resource), + toJsonExample([]string{"key", "\"null\""}, resource), + toJsonExample([]string{"key[0]", "a", "key[1]", "true"}, resource), + toJsonExample([]string{"key.some.child", "hello", "key.some.other", "goodbye"}, resource), + ) +} + +func toJsonExample(in []string, resource resources.Resource) string { + + if !resource.NoWrapping { + in = append([]string{"type", resource.JsonApiType}, in...) + } + + jsonTxt, err := json.ToJson(in, resource.NoWrapping, resource.JsonApiFormat == "compliant", resource.Attributes) + + if err != nil { + return fmt.Sprintf("Could not get json: %s", err) + } + + return jsonTxt +} + +func GetCreateLong(resource resources.Resource) string { + resourceName := resource.SingularName + + singularTypeNames, err := resources.GetSingularTypesOfVariablesNeeded(resource.CreateEntityInfo.Url) + + if err != nil { + return fmt.Sprintf("Could not generate usage string: %s", err) + } + + parametersLongUsage := GetParameterUsageForTypes(singularTypeNames) + + argumentsBlurb := "" + switch resource.CreateEntityInfo.ContentType { + case "multipart/form-data": + argumentsBlurb = "Key and values are passed in using multipart/form-data encoding\n\nDocumentation:\n " + resource.CreateEntityInfo.Docs + case "application/json", "": + argumentsBlurb = fmt.Sprintf(` +%s + +Documentation: + %s +`, GetJsonSyntaxExample(resource), resource.CreateEntityInfo.Docs) + default: + argumentsBlurb = fmt.Sprintf("This resource uses %s encoding, which this help doesn't know how to help you with :) Submit a bug please.\nDocumentation:\n %s", resource.CreateEntityInfo.ContentType, resource.CreateEntityInfo.Docs) + } + + return fmt.Sprintf(`Creates a %s in a store/organization by calling %s. +%s +%s +`, resourceName, GetHelpResourceUrls(resource.CreateEntityInfo.Url), parametersLongUsage, argumentsBlurb) +} + +func GetUpdateLong(resource resources.Resource) string { + resourceName := resource.SingularName + + singularTypeNames, err := resources.GetSingularTypesOfVariablesNeeded(resource.UpdateEntityInfo.Url) + + if err != nil { + return fmt.Sprintf("Could not generate usage string: %s", err) + } + + parametersLongUsage := GetParameterUsageForTypes(singularTypeNames) + + argumentsBlurb := "" + switch resource.UpdateEntityInfo.ContentType { + case "multipart/form-data": + argumentsBlurb = "Key and values are passed in using multipart/form-data encoding\n\nDocumentation:\n " + resource.DeleteEntityInfo.Docs + case "application/json", "": + argumentsBlurb = fmt.Sprintf(` +Key and value pairs passed in will be converted to JSON with a jq like syntax. + +The EPCC CLI will automatically determine appropriate wrapping + +Basic Types: +key b => { "a": "b" } +key 1 => { "a": 1 } +key '"1"' => { "a": "1" } +key true => { "a": true } +key null => { "a": null } +key '"null"'' => { "a": "null" } + + + +Documentation: + %s +`, resource.UpdateEntityInfo.Docs) + default: + argumentsBlurb = fmt.Sprintf("This resource uses %s encoding, which this help doesn't know how to help you with :) Submit a bug please.\nDocumentation:\n %s", resource.UpdateEntityInfo.ContentType, resource.UpdateEntityInfo.Docs) + } + + return fmt.Sprintf(`Updates a %s in a store/organization by calling %s. +%s +%s +`, resourceName, GetHelpResourceUrls(resource.UpdateEntityInfo.Url), parametersLongUsage, argumentsBlurb) +} + +func GetDeleteLong(resource resources.Resource) string { + resourceName := resource.SingularName + + singularTypeNames, err := resources.GetSingularTypesOfVariablesNeeded(resource.DeleteEntityInfo.Url) + + if err != nil { + return fmt.Sprintf("Could not generate usage string: %s", err) + } + + parametersLongUsage := GetParameterUsageForTypes(singularTypeNames) + + argumentsBlurb := "" + switch resource.DeleteEntityInfo.ContentType { + case "multipart/form-data": + argumentsBlurb = "Key and values are passed in using multipart/form-data encoding\n\nDocumentation:\n " + resource.DeleteEntityInfo.Docs + case "application/json", "": + argumentsBlurb = fmt.Sprintf(` +Key and value pairs passed in will be converted to JSON with a jq like syntax. + +The EPCC CLI will automatically determine appropriate wrapping + +Basic Types: +key b => { "a": "b" } +key 1 => { "a": 1 } +key '"1"' => { "a": "1" } +key true => { "a": true } +key null => { "a": null } +key '"null"'' => { "a": "null" } + + + +Documentation: + %s +`, resource.DeleteEntityInfo.Docs) + default: + argumentsBlurb = fmt.Sprintf("This resource uses %s encoding, which this help doesn't know how to help you with :) Submit a bug please.\nDocumentation:\n %s", resource.DeleteEntityInfo.ContentType, resource.DeleteEntityInfo.Docs) + } + + return fmt.Sprintf(`Deletes a %s in a store/organization by calling %s. +%s +%s +`, resourceName, GetHelpResourceUrls(resource.DeleteEntityInfo.Url), parametersLongUsage, argumentsBlurb) +} + +func GetGetUsageString(resourceName string, resourceUrl string, completionVerb int, resource resources.Resource) string { + singularTypeNames, err := resources.GetSingularTypesOfVariablesNeeded(resourceUrl) + + if err != nil { + log.Warnf("Could not generate usage string for %s, error %v", resourceName, err) + return resourceName + } + + usageString := resourceName + GetParametersForTypes(singularTypeNames) + + queryParameters, _ := completion.Complete(completion.Request{ + Type: completion.CompleteQueryParamKey, + Resource: resource, + Verb: completionVerb, + }) + + for _, qp := range queryParameters { + if qp == "" { + continue + } + + switch qp { + case "page[limit]": + usageString += fmt.Sprintf(" [page[limit] N]") + case "page[offset]": + // No example + usageString += fmt.Sprintf(" [page[offset] N]") + case "sort": + usageString += fmt.Sprintf(" [sort SORT]") + case "filter": + usageString += fmt.Sprintf(" [filter FILTER]") + default: + usageString += fmt.Sprintf(" [%s VALUE]", qp) + } + + } + + return usageString +} +func GetCreateUsageString(resource resources.Resource) string { + resourceName := resource.SingularName + + singularTypeNames, err := resources.GetSingularTypesOfVariablesNeeded(resource.CreateEntityInfo.Url) + + if err != nil { + log.Warnf("Could not generate usage string for %s, error %v", resourceName, err) + return resourceName + } + + return resourceName + GetParametersForTypes(singularTypeNames) + GetJsonKeyValuesForUsage(resource) +} + +func GetUpdateUsage(resource resources.Resource) string { + resourceName := resource.SingularName + + singularTypeNames, err := resources.GetSingularTypesOfVariablesNeeded(resource.UpdateEntityInfo.Url) + + if err != nil { + log.Warnf("Could not generate usage string for %s, error %v", resourceName, err) + return resourceName + } + + return resourceName + GetParametersForTypes(singularTypeNames) + GetJsonKeyValuesForUsage(resource) +} + +func GetDeleteUsage(resource resources.Resource) string { + resourceName := resource.SingularName + + singularTypeNames, err := resources.GetSingularTypesOfVariablesNeeded(resource.DeleteEntityInfo.Url) + + if err != nil { + log.Warnf("Could not generate usage string for %s, error %v", resourceName, err) + return resourceName + } + + return resourceName + GetParametersForTypes(singularTypeNames) + GetJsonKeyValuesForUsage(resource) +} + +var getExampleCache sync.Map + +func GetGetExample(resourceName string, resourceUrl string, usageGetType string, completionVerb int, urlInfo *resources.CrudEntityInfo, resource resources.Resource) string { + + cacheKey := fmt.Sprintf("%s-%d", resourceName, completionVerb) + if example, ok := getExampleCache.Load(cacheKey); ok { + return example.(string) + } + + singularTypeNames, err := resources.GetSingularTypesOfVariablesNeeded(resourceUrl) + + if err != nil { + return fmt.Sprintf("Could not generate example: %s", err) + } + + uuids := GetUuidsForTypes(singularTypeNames) + exampleWithIds := fmt.Sprintf(" epcc get %s %s", resourceName, GetArgumentExampleWithIds(singularTypeNames, uuids)) + exampleWithAliases := fmt.Sprintf(" epcc get %s %s", resourceName, GetArgumentExampleWithAlias(singularTypeNames)) + + examples := fmt.Sprintf(" # Retrieve %s %s\n%s\n > GET %s\n\n", usageGetType, resourceName, exampleWithIds, FillUrlWithIds(urlInfo, uuids)) + + if len(singularTypeNames) > 0 { + examples += fmt.Sprintf(" # Retrieve %s %s using aliases \n%s\n > GET %s\n\n", usageGetType, resourceName, exampleWithAliases, FillUrlWithIds(urlInfo, uuids)) + } + + queryParameters, _ := completion.Complete(completion.Request{ + Type: completion.CompleteQueryParamKey, + Resource: resource, + Verb: completionVerb, + }) + + for _, qp := range queryParameters { + if qp == "" { + continue + } + + switch qp { + case "page[limit]": + examples += fmt.Sprintf(" # Retrieve %s %s with page[limit] = 25 and page[offset] = 500 \n%s %s %s %s %s \n > GET %s \n\n", usageGetType, resourceName, exampleWithAliases, qp, "25", "page[offset]", "500", FillUrlWithIds(urlInfo, uuids)+"?page[limit]=25&page[offset]=500") + + case "sort": + + sortKeys, _ := completion.Complete(completion.Request{ + Type: completion.CompleteQueryParamValue, + Resource: resource, + QueryParam: "sort", + Verb: completionVerb, + }) + + rand.Shuffle(len(sortKeys), func(i, j int) { + sortKeys[i], sortKeys[j] = sortKeys[j], sortKeys[i] + }) + + for i, v := range sortKeys { + if v[0] != '-' { + examples += fmt.Sprintf(" # Retrieve %s %s sorted in ascending order of %s\n%s %s %s \n > GET %s\n\n", usageGetType, resourceName, v, exampleWithAliases, qp, v, FillUrlWithIds(urlInfo, uuids)+"?sort="+v) + } else { + examples += fmt.Sprintf(" # Retrieve %s %s sorted in descending order of %s\n%s %s -- %s\n > GET %s\n\n", usageGetType, resourceName, v, exampleWithAliases, qp, v, FillUrlWithIds(urlInfo, uuids)+"?sort="+v) + } + + if i > 2 { + // Only need three examples for sort + break + } + } + + case "filter": + + attributeKeys, _ := completion.Complete(completion.Request{ + Type: completion.CompleteAttributeKey, + Resource: resource, + Attributes: map[string]int{}, + Verb: completion.Create, + }) + + rand.Shuffle(len(attributeKeys), func(i, j int) { + attributeKeys[i], attributeKeys[j] = attributeKeys[j], attributeKeys[i] + }) + + searchOps := []string{"eq", "like", "gt"} + for i, v := range attributeKeys { + examples += fmt.Sprintf(` # Retrieve %s %s with filter %s(%s,"Hello World") + %s %s '%s(%s,"Hello World")' + > GET %s + +`, usageGetType, resourceName, searchOps[i], v, exampleWithAliases, qp, searchOps[i], v, FillUrlWithIds(urlInfo, uuids)+fmt.Sprintf(`?filter=%s(%s,"Hello World")`, searchOps[i], v)) + + if i >= 2 { + // Only need three examples for sort + break + } + } + + default: + + examples += fmt.Sprintf(" # Retrieve %s %s with a(n) %s = %s\n%s %s %s \n > GET %s \n\n", usageGetType, resourceName, qp, "x", exampleWithAliases, qp, "x", FillUrlWithIds(urlInfo, uuids)+"?"+qp+"=x") + } + + } + + example := strings.ReplaceAll(strings.Trim(examples, "\n"), " ", " ") + + getExampleCache.Store(cacheKey, example) + + return example +} + +var createExampleCache sync.Map + +func GetCreateExample(resource resources.Resource) string { + resourceName := resource.SingularName + + if v, ok := createExampleCache.Load(resourceName); ok { + return v.(string) + } + + singularTypeNames, err := resources.GetSingularTypesOfVariablesNeeded(resource.CreateEntityInfo.Url) + + if err != nil { + return fmt.Sprintf("Could not generate example: %s", err) + } + + uuids := GetUuidsForTypes(singularTypeNames) + exampleWithIds := fmt.Sprintf(" epcc create %s %s", resourceName, GetArgumentExampleWithIds(singularTypeNames, uuids)) + + exampleWithAliases := fmt.Sprintf(" epcc create %s %s", resourceName, GetArgumentExampleWithAlias(singularTypeNames)) + + baseJsonArgs := []string{} + if !resource.NoWrapping { + baseJsonArgs = append(baseJsonArgs, "type", resource.JsonApiType) + } + + emptyJson, _ := json.ToJson(baseJsonArgs, resource.NoWrapping, resource.JsonApiFormat == "compliant", resource.Attributes) + + examples := GetJsonExample(fmt.Sprintf("# Create a %s", resource.SingularName), exampleWithIds, fmt.Sprintf("> POST %s", FillUrlWithIds(resource.CreateEntityInfo, uuids)), emptyJson) + + if len(singularTypeNames) > 0 { + examples += GetJsonExample(fmt.Sprintf("# Create a %s using aliases", resource.SingularName), exampleWithIds, fmt.Sprintf("> POST %s", FillUrlWithIds(resource.CreateEntityInfo, uuids)), emptyJson) + } + + if resource.CreateEntityInfo.ContentType != "multipart/form-data" { + for k := range resource.Attributes { + + if k[0] == '^' { + continue + } + + results, _ := completion.Complete(completion.Request{ + Type: completion.CompleteAttributeValue, + Resource: resource, + Verb: completion.Create, + Attribute: k, + ToComplete: "", + }) + + arg := `"Hello World"` + + if len(results) > 0 { + arg = results[0] + } + + extendedArgs := append(baseJsonArgs, k, arg) + + // Don't try and use more than one key as some are mutually exclusive and the JSON will crash. + // Resources that are heterogenous and can have array or object fields at some level (i.e., data[n].id and data.id) are examples + jsonTxt, _ := json.ToJson(extendedArgs, resource.NoWrapping, resource.JsonApiFormat == "compliant", resource.Attributes) + examples += GetJsonExample(fmt.Sprintf("# Create a %s passing in an argument", resourceName), fmt.Sprintf("%s %s %s", exampleWithAliases, k, arg), fmt.Sprintf("> POST %s", FillUrlWithIds(resource.CreateEntityInfo, uuids)), jsonTxt) + + autofilledData := autofill.GetJsonArrayForResource(&resource) + + extendedArgs = append(autofilledData, extendedArgs...) + + jsonTxt, _ = json.ToJson(extendedArgs, resource.NoWrapping, resource.JsonApiFormat == "compliant", resource.Attributes) + examples += GetJsonExample(fmt.Sprintf("# Create a %s (using --auto-fill) and passing in an argument", resourceName), fmt.Sprintf("%s --auto-fill %s %s", exampleWithAliases, k, arg), fmt.Sprintf("> POST %s", FillUrlWithIds(resource.CreateEntityInfo, uuids)), jsonTxt) + + break + } + } + + example := strings.ReplaceAll(strings.Trim(examples, "\n"), " ", " ") + + createExampleCache.Store(resourceName, example) + + return example +} + +var updateExampleCache sync.Map + +func GetUpdateExample(resource resources.Resource) string { + resourceName := resource.SingularName + + if v, ok := updateExampleCache.Load(resourceName); ok { + return v.(string) + } + singularTypeNames, err := resources.GetSingularTypesOfVariablesNeeded(resource.UpdateEntityInfo.Url) + + if err != nil { + return fmt.Sprintf("Could not generate example: %s", err) + } + + uuids := GetUuidsForTypes(singularTypeNames) + exampleWithIds := fmt.Sprintf(" epcc update %s %s", resourceName, GetArgumentExampleWithIds(singularTypeNames, uuids)) + exampleWithAliases := fmt.Sprintf(" epcc update %s %s", resourceName, GetArgumentExampleWithAlias(singularTypeNames)) + + baseJsonArgs := []string{} + if !resource.NoWrapping { + baseJsonArgs = append(baseJsonArgs, "type", resource.JsonApiType) + } + + emptyJson, _ := json.ToJson(baseJsonArgs, resource.NoWrapping, resource.JsonApiFormat == "compliant", resource.Attributes) + + examples := GetJsonExample(fmt.Sprintf("# Update a %s", resource.SingularName), exampleWithIds, fmt.Sprintf("> PUT %s", FillUrlWithIds(resource.UpdateEntityInfo, uuids)), emptyJson) + + if len(singularTypeNames) > 0 { + examples += GetJsonExample(fmt.Sprintf("# Update a %s using aliases", resource.SingularName), exampleWithIds, fmt.Sprintf("> PUT %s", FillUrlWithIds(resource.UpdateEntityInfo, uuids)), emptyJson) + } + + if resource.UpdateEntityInfo.ContentType != "multipart/form-data" { + for k := range resource.Attributes { + + if k[0] == '^' { + continue + } + + results, _ := completion.Complete(completion.Request{ + Type: completion.CompleteAttributeValue, + Resource: resource, + Verb: completion.Update, + Attribute: k, + ToComplete: "", + }) + + arg := `"Hello World"` + + if len(results) > 0 { + arg = results[0] + } + + extendedArgs := append(baseJsonArgs, k, arg) + + // Don't try and use more than one key as some are mutually exclusive and the JSON will crash. + // Resources that are heterogenous and can have array or object fields at some level (i.e., data[n].id and data.id) are examples + jsonTxt, _ := json.ToJson(extendedArgs, resource.NoWrapping, resource.JsonApiFormat == "compliant", resource.Attributes) + examples += GetJsonExample(fmt.Sprintf("# update a %s passing in an argument", resourceName), fmt.Sprintf("%s %s %s", exampleWithAliases, k, arg), fmt.Sprintf("> PUT %s", FillUrlWithIds(resource.UpdateEntityInfo, uuids)), jsonTxt) + + break + } + } + + example := strings.ReplaceAll(strings.Trim(examples, "\n"), " ", " ") + updateExampleCache.Store(resourceName, example) + return example +} + +var deleteExampleCache sync.Map + +func GetDeleteExample(resource resources.Resource) string { + resourceName := resource.SingularName + if v, ok := deleteExampleCache.Load(resourceName); ok { + return v.(string) + } + + singularTypeNames, err := resources.GetSingularTypesOfVariablesNeeded(resource.DeleteEntityInfo.Url) + + if err != nil { + return fmt.Sprintf("Could not generate example: %s", err) + } + + uuids := GetUuidsForTypes(singularTypeNames) + exampleWithIds := fmt.Sprintf(" epcc delete %s %s", resourceName, GetArgumentExampleWithIds(singularTypeNames, uuids)) + exampleWithAliases := fmt.Sprintf(" epcc delete %s %s", resourceName, GetArgumentExampleWithAlias(singularTypeNames)) + + baseJsonArgs := []string{} + if !resource.NoWrapping { + baseJsonArgs = append(baseJsonArgs, "type", resource.JsonApiType) + } + + emptyJson, _ := json.ToJson(baseJsonArgs, resource.NoWrapping, resource.JsonApiFormat == "compliant", resource.Attributes) + + examples := GetJsonExample(fmt.Sprintf("# Delete a %s", resource.SingularName), exampleWithIds, fmt.Sprintf("> PUT %s", FillUrlWithIds(resource.DeleteEntityInfo, uuids)), emptyJson) + + if len(singularTypeNames) > 0 { + examples += GetJsonExample(fmt.Sprintf("# Delete a %s using aliases", resource.SingularName), exampleWithIds, fmt.Sprintf("> PUT %s", FillUrlWithIds(resource.DeleteEntityInfo, uuids)), emptyJson) + } + + if resource.DeleteEntityInfo.ContentType != "multipart/form-data" { + for k := range resource.Attributes { + + if k[0] == '^' { + continue + } + + results, _ := completion.Complete(completion.Request{ + Type: completion.CompleteAttributeValue, + Resource: resource, + Verb: completion.Delete, + Attribute: k, + ToComplete: "", + }) + + arg := `"Hello World"` + + if len(results) > 0 { + arg = results[0] + } + + extendedArgs := append(baseJsonArgs, k, arg) + + // Don't try and use more than one key as some are mutually exclusive and the JSON will crash. + // Resources that are heterogenous and can have array or object fields at some level (i.e., data[n].id and data.id) are examples + jsonTxt, _ := json.ToJson(extendedArgs, resource.NoWrapping, resource.JsonApiFormat == "compliant", resource.Attributes) + examples += GetJsonExample(fmt.Sprintf("# delete a %s passing in an argument", resourceName), fmt.Sprintf("%s %s %s", exampleWithAliases, k, arg), fmt.Sprintf("> DELETE %s", FillUrlWithIds(resource.DeleteEntityInfo, uuids)), jsonTxt) + + break + } + } + + example := strings.ReplaceAll(strings.Trim(examples, "\n"), " ", " ") + + deleteExampleCache.Store(resourceName, example) + + return example +} + +func getCommandForResource(cmd *cobra.Command, res string) *cobra.Command { + for _, c := range cmd.Commands() { + if strings.HasPrefix(c.Use, res+" ") { + return c + } + } + return nil +} diff --git a/cmd/root.go b/cmd/root.go index ed185f5..029fe7a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -12,7 +12,6 @@ import ( "github.com/elasticpath/epcc-cli/external/version" log "github.com/sirupsen/logrus" "github.com/thediveo/enumflag" - "golang.org/x/time/rate" "os" "os/signal" "syscall" @@ -54,13 +53,15 @@ var jqCompletionFunc = func(cmd *cobra.Command, args []string, toComplete string }, cobra.ShellCompDirectiveNoSpace } -func init() { +func InitializeCmd() { cobra.OnInitialize(initConfig) + initConfig() if err := env.Parse(config.Envs); err != nil { panic("Could not parse environment variables") } + applyLogLevelEarlyDetectionHack() initRunbookCommands() RootCmd.AddCommand( cmCommand, @@ -85,16 +86,13 @@ func init() { Logs.AddCommand(LogsList, LogsShow, LogsClear) testJson.Flags().BoolVarP(&noWrapping, "no-wrapping", "", false, "if set, we won't wrap the output the json in a data tag") - testJson.Flags().BoolVarP(&compliant, "compliant", "", false, "if set, we wrap most keys in an attributes tage automatically.") + testJson.Flags().BoolVarP(&compliant, "compliant", "", false, "if set, we wrap most keys in an attributes tags automatically.") - RootCmd.PersistentFlags().Var( - enumflag.New(&logger.Loglevel, "log", logger.LoglevelIds, enumflag.EnumCaseInsensitive), - "log", - "sets logging level; can be 'trace', 'debug', 'info', 'warn', 'error', 'fatal', 'panic'") + addLogLevel(RootCmd) RootCmd.PersistentFlags().BoolVarP(&json.MonochromeOutput, "monochrome-output", "M", false, "By default, epcc will output using colors if the terminal supports this. Use this option to disable it.") RootCmd.PersistentFlags().StringSliceVarP(&httpclient.RawHeaders, "header", "H", []string{}, "Extra headers and values to include in the request when sending HTTP to a server. You may specify any number of extra headers.") - RootCmd.PersistentFlags().StringVarP(&profiles.ProfileName, "profile", "P", "default", "overrides the current EPCC_PROFILE var to run the command with the chosen profile.") + RootCmd.PersistentFlags().StringVarP(&profiles.ProfileName, "profile", "P", profiles.ProfileName, "overrides the current EPCC_PROFILE var to run the command with the chosen profile.") RootCmd.PersistentFlags().Uint16VarP(&rateLimit, "rate-limit", "", 10, "Request limit per second") RootCmd.PersistentFlags().BoolVarP(&httpclient.Retry5xx, "retry-5xx", "", false, "Whether we should retry requests with HTTP 5xx response code") RootCmd.PersistentFlags().BoolVarP(&httpclient.Retry429, "retry-429", "", false, "Whether we should retry requests with HTTP 429 response code") @@ -119,6 +117,39 @@ func init() { logoutCmd.AddCommand(logoutAccountManagement) } +// If there is a log level argument, we will set it much earlier on a dummy command +// this helps if you need to enable tracing while the root command is being built. +func applyLogLevelEarlyDetectionHack() { + for i, arg := range os.Args { + if arg == "--log" && i+1 < len(os.Args) { + newCmd := &cobra.Command{ + Use: "foo", + } + addLogLevel(newCmd) + + newCmd.SetArgs([]string{"--log", os.Args[i+1]}) + + newCmd.RunE = func(command *cobra.Command, args []string) error { + log.SetLevel(logger.Loglevel) + return nil + } + + err := newCmd.Execute() + if err != nil { + log.Warnf("Couldn't set log level early: %v", err) + } + return + } + } +} + +func addLogLevel(cmd *cobra.Command) { + cmd.PersistentFlags().Var( + enumflag.New(&logger.Loglevel, "log", logger.LoglevelIds, enumflag.EnumCaseInsensitive), + "log", + "sets logging level; can be 'trace', 'debug', 'info', 'warn', 'error', 'fatal', 'panic'") +} + var persistentPreRunFuncs []func(cmd *cobra.Command, args []string) error func AddRootPreRunFunc(f func(cmd *cobra.Command, args []string) error) { @@ -151,8 +182,7 @@ Environment Variables rateLimit = config.Envs.EPCC_RATE_LIMIT } log.Debugf("Rate limit set to %d request per second ", rateLimit) - httpclient.Limit = rate.NewLimiter(rate.Limit(rateLimit), 1) - httpclient.HttpClient.Timeout = time.Duration(int64(requestTimeout*1000) * int64(time.Millisecond)) + httpclient.Initialize(rateLimit, requestTimeout) for _, runFunc := range persistentPreRunFuncs { err := runFunc(cmd, args) diff --git a/cmd/runbooks.go b/cmd/runbooks.go index d4b8820..57ea81b 100644 --- a/cmd/runbooks.go +++ b/cmd/runbooks.go @@ -157,6 +157,7 @@ func initRunbookRunCommands() *cobra.Command { rawCmdLines, err := runbooks.RenderTemplates(templateName, rawCmd, runbookStringArguments, runbookAction.Variables) if err != nil { + cancelFunc() return err } resultChan := make(chan *commandResult, *maxConcurrency*2) @@ -179,6 +180,7 @@ func initRunbookRunCommands() *cobra.Command { rawCmdArguments, err := shellwords.SplitPosix(strings.Trim(rawCmdLine, " \n")) if err != nil { + cancelFunc() return err } @@ -214,11 +216,12 @@ func initRunbookRunCommands() *cobra.Command { } fn := fn - semaphore.Acquire(context.TODO(), 1) - go func() { - defer semaphore.Release(1) - fn() - }() + if err := semaphore.Acquire(ctx, 1); err == nil { + go func() { + defer semaphore.Release(1) + fn() + }() + } } }() @@ -328,7 +331,7 @@ func initRunbookDevCommands() *cobra.Command { func getDevCommands(parent *cobra.Command) { parent.AddCommand(&cobra.Command{ - Use: "sleep time", + Use: "sleep", Short: "Sleep for a predefined duration", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { @@ -338,8 +341,7 @@ func getDevCommands(parent *cobra.Command) { if err != nil { return fmt.Errorf("could not sleep due to error: %v", err) } - - log.Debugf("Sleeping for %d seconds", timeToSleep) + log.Infof("Sleeping for %d seconds", timeToSleep) time.Sleep(time.Duration(timeToSleep) * time.Second) return nil diff --git a/cmd/update.go b/cmd/update.go index 713d4a1..d3b5bae 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -25,109 +25,122 @@ func NewUpdateCommand(parentCmd *cobra.Command) { var outputJq = "" - var update = &cobra.Command{ - Use: "update [PARENT_ID_1] [PARENT_ID_2] [ID]... ...", - Short: "Updates an entity of a resource.", - Args: cobra.MinimumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { + var noBodyPrint = false - body, err := updateInternal(context.Background(), overrides, args) + var update = &cobra.Command{ + Use: "update", + Short: "Updates a resource", + SilenceUsage: false, + } + for _, resource := range resources.GetPluralResources() { + resource := resource + resourceName := resource.SingularName + if resource.UpdateEntityInfo == nil { + continue + } - if err != nil { - return err - } + var updateResourceCmd = &cobra.Command{ + Use: GetUpdateUsage(resource), + Short: GetUpdateShort(resource), + Long: GetUpdateLong(resource), + Example: GetUpdateExample(resource), + Args: GetArgFunctionForUpdate(resource), + RunE: func(cmd *cobra.Command, args []string) error { - if outputJq != "" { - output, err := json.RunJQOnStringWithArray(outputJq, body) + body, err := updateInternal(context.Background(), overrides, append([]string{resourceName}, args...)) if err != nil { return err } - for _, outputLine := range output { - outputJson, err := gojson.Marshal(outputLine) + if outputJq != "" { + output, err := json.RunJQOnStringWithArray(outputJq, body) if err != nil { return err } - err = json.PrintJson(string(outputJson)) - - if err != nil { - return err - } - } + for _, outputLine := range output { + outputJson, err := gojson.Marshal(outputLine) - return nil - } - - return json.PrintJson(body) - }, - - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if len(args) == 0 { - return completion.Complete(completion.Request{ - Type: completion.CompleteSingularResource, - Verb: completion.Update, - }) - } - - // Find Resource - resource, ok := resources.GetResourceByName(args[0]) - if ok { - if resource.UpdateEntityInfo != nil { - resourceURL := resource.UpdateEntityInfo.Url - idCount, _ := resources.GetNumberOfVariablesNeeded(resourceURL) - if len(args)-idCount >= 1 { // Arg is after IDs - if (len(args)-idCount)%2 == 1 { // This is an attribute key - usedAttributes := make(map[string]int) - for i := idCount + 1; i < len(args); i = i + 2 { - usedAttributes[args[i]] = 0 - } - return completion.Complete(completion.Request{ - Type: completion.CompleteAttributeKey, - Resource: resource, - Attributes: usedAttributes, - Verb: completion.Update, - }) - } else { // This is an attribute value - return completion.Complete(completion.Request{ - Type: completion.CompleteAttributeValue, - Resource: resource, - Verb: completion.Update, - Attribute: args[len(args)-1], - ToComplete: toComplete, - }) + if err != nil { + return err } - } else { - // Arg is in IDS - // Must be for a resource completion - types, err := resources.GetTypesOfVariablesNeeded(resourceURL) + + err = json.PrintJson(string(outputJson)) if err != nil { - return []string{}, cobra.ShellCompDirectiveNoFileComp + return err } + } - typeIdxNeeded := len(args) - 1 + return nil + } + + if noBodyPrint { + return nil + } else { + return json.PrintJson(body) + } - if completionResource, ok := resources.GetResourceByName(types[typeIdxNeeded]); ok { - return completion.Complete(completion.Request{ - Type: completion.CompleteAlias, - Resource: completionResource, - }) + }, + + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + // Find Resource + resourceURL := resource.UpdateEntityInfo.Url + idCount, _ := resources.GetNumberOfVariablesNeeded(resourceURL) + if len(args)-idCount >= 0 { // Arg is after IDs + if (len(args)-idCount)%2 == 0 { // This is an attribute key + usedAttributes := make(map[string]int) + for i := idCount; i < len(args); i = i + 2 { + usedAttributes[args[i]] = 0 } + return completion.Complete(completion.Request{ + Type: completion.CompleteAttributeKey, + Resource: resource, + Attributes: usedAttributes, + Verb: completion.Update, + }) + } else { // This is an attribute value + return completion.Complete(completion.Request{ + Type: completion.CompleteAttributeValue, + Resource: resource, + Verb: completion.Update, + Attribute: args[len(args)-1], + ToComplete: toComplete, + }) + } + } else { + // Arg is in IDS + // Must be for a resource completion + types, err := resources.GetTypesOfVariablesNeeded(resourceURL) + + if err != nil { + return []string{}, cobra.ShellCompDirectiveNoFileComp + } + + typeIdxNeeded := len(args) + + if completionResource, ok := resources.GetResourceByName(types[typeIdxNeeded]); ok { + return completion.Complete(completion.Request{ + Type: completion.CompleteAlias, + Resource: completionResource, + }) } } - } + return []string{}, cobra.ShellCompDirectiveNoFileComp + }, + } + + updateResourceCmd.Flags().StringVar(&overrides.OverrideUrlPath, "override-url-path", "", "Override the URL that will be used for the Request") + updateResourceCmd.Flags().StringSliceVarP(&overrides.QueryParameters, "query-parameters", "q", []string{}, "Pass in key=value an they will be added as query parameters") + updateResourceCmd.PersistentFlags().BoolVarP(&noBodyPrint, "silent", "s", false, "Don't print the body on success") + updateResourceCmd.Flags().StringVarP(&outputJq, "output-jq", "", "", "A jq expression, if set we will restrict output to only this") + _ = updateResourceCmd.RegisterFlagCompletionFunc("output-jq", jqCompletionFunc) - return []string{}, cobra.ShellCompDirectiveNoFileComp - }, + update.AddCommand(updateResourceCmd) } - update.Flags().StringVar(&overrides.OverrideUrlPath, "override-url-path", "", "Override the URL that will be used for the Request") - update.Flags().StringSliceVarP(&overrides.QueryParameters, "query-parameters", "q", []string{}, "Pass in key=value an they will be added as query parameters") - update.Flags().StringVarP(&outputJq, "output-jq", "", "", "A jq expression, if set we will restrict output to only this") - _ = update.RegisterFlagCompletionFunc("output-jq", jqCompletionFunc) parentCmd.AddCommand(update) } diff --git a/cmd/update_test.go b/cmd/update_test.go new file mode 100644 index 0000000..625414f --- /dev/null +++ b/cmd/update_test.go @@ -0,0 +1,199 @@ +package cmd + +import ( + "github.com/elasticpath/epcc-cli/external/aliases" + "github.com/spf13/cobra" + "github.com/stretchr/testify/require" + "testing" +) + +func TestUpdateCompletionReturnsFirstElementParentId(t *testing.T) { + + // Fixture Setup + err := aliases.ClearAllAliases() + + require.NoError(t, err) + + aliases.SaveAliasesForResources( + // language=JSON + ` +{ + "data": { + "id": "123", + "type": "account", + "name": "John" + } +}`) + + rootCmd := &cobra.Command{} + NewUpdateCommand(rootCmd) + updateCmd := getCommandForResource(rootCmd.Commands()[0], "account-address") + + require.NotNil(t, updateCmd, "Update command for account-addresses should exist") + + // Execute SUT + completionResult, _ := updateCmd.ValidArgsFunction(updateCmd, []string{}, "") + + // Verify + require.Contains(t, completionResult, "name=John") +} + +func TestUpdateCompletionReturnsSecondElementId(t *testing.T) { + + // Fixture Setup + err := aliases.ClearAllAliases() + + require.NoError(t, err) + + aliases.SaveAliasesForResources( + // language=JSON + ` +{ + "data": { + "id": "123", + "type": "address" + } +}`) + + rootCmd := &cobra.Command{} + NewUpdateCommand(rootCmd) + updateCmd := getCommandForResource(rootCmd.Commands()[0], "account-address") + + require.NotNil(t, updateCmd, "Update command for account-addresses should exist") + + // Execute SUT + completionResult, _ := updateCmd.ValidArgsFunction(updateCmd, []string{"name=John"}, "") + + // Verify + require.Contains(t, completionResult, "id=123") +} + +func TestUpdateCompletionReturnsAnValidAttributeKey(t *testing.T) { + + // Fixture Setup + rootCmd := &cobra.Command{} + NewUpdateCommand(rootCmd) + updateCmd := getCommandForResource(rootCmd.Commands()[0], "account-address") + + require.NotNil(t, updateCmd, "Update command for account-addresses should exist") + + // Execute SUT + completionResult, _ := updateCmd.ValidArgsFunction(updateCmd, []string{"name=John", "id=123"}, "") + + // Verify + require.Contains(t, completionResult, "county") + require.Contains(t, completionResult, "city") +} + +func TestUpdateCompletionReturnsAnValidAttributeKeyThatHasNotBeenUsed(t *testing.T) { + + // Fixture Setup + rootCmd := &cobra.Command{} + NewUpdateCommand(rootCmd) + updateCmd := getCommandForResource(rootCmd.Commands()[0], "account-address") + + require.NotNil(t, updateCmd, "Update command for account-addresses should exist") + + // Execute SUT + completionResult, _ := updateCmd.ValidArgsFunction(updateCmd, []string{"name=John", "id=123", "city", "Aylesbury"}, "") + + // Verify + require.Contains(t, completionResult, "county") + require.NotContains(t, completionResult, "city") +} + +func TestUpdateCompletionReturnsAnValidAttributeValue(t *testing.T) { + + // Fixture Setup + rootCmd := &cobra.Command{} + NewUpdateCommand(rootCmd) + updateCmd := getCommandForResource(rootCmd.Commands()[0], "authentication-realm") + + require.NotNil(t, updateCmd, "Update command for account-addresses should exist") + + // Execute SUT + completionResult, _ := updateCmd.ValidArgsFunction(updateCmd, []string{"id=123", "duplicate_email_policy"}, "") + + // Verify + require.Contains(t, completionResult, "allowed") + require.Contains(t, completionResult, "api_only") + +} + +func TestUpdateArgFunctionForEntityUrlHasErrorWithNoArgs(t *testing.T) { + // Fixture Setup + resourceName := "account" + + rootCmd := &cobra.Command{} + NewUpdateCommand(rootCmd) + updateCmd := getCommandForResource(rootCmd.Commands()[0], resourceName) + + // Execute SUT + err := updateCmd.Args(updateCmd, []string{}) + + // Verification + require.ErrorContains(t, err, "ACCOUNT_ID must be specified") +} + +func TestUpdateArgFunctionForEntityUrlWithParentIdHasErrorWithNoArgs(t *testing.T) { + // Fixture Setup + resourceName := "account-address" + + rootCmd := &cobra.Command{} + NewUpdateCommand(rootCmd) + updateCmd := getCommandForResource(rootCmd.Commands()[0], resourceName) + + // Execute SUT + err := updateCmd.Args(updateCmd, []string{}) + + // Verification + require.ErrorContains(t, err, "ACCOUNT_ID, ACCOUNT_ADDRESS_ID must be specified") + +} + +func TestUpdateArgFunctionForEntityUrlHasNoErrorWithArgs(t *testing.T) { + // Fixture Setup + resourceName := "account" + + rootCmd := &cobra.Command{} + NewUpdateCommand(rootCmd) + updateCmd := getCommandForResource(rootCmd.Commands()[0], resourceName) + + // Execute SUT + err := updateCmd.Args(updateCmd, []string{"foo"}) + + // Verification + require.NoError(t, err) + +} + +func TestUpdateArgFunctionForEntityUrlWithParentIdHasErrorWithOneArgOnly(t *testing.T) { + // Fixture Setup + resourceName := "account-address" + + rootCmd := &cobra.Command{} + NewUpdateCommand(rootCmd) + updateCmd := getCommandForResource(rootCmd.Commands()[0], resourceName) + + // Execute SUT + err := updateCmd.Args(updateCmd, []string{"foo"}) + + // Verification + require.ErrorContains(t, err, "ACCOUNT_ADDRESS_ID must be specified") + require.NotContains(t, err.Error(), "ACCOUNT_ID must be specified") +} + +func TestUpdateArgFunctionForEntityUrlWithParentIdHasNoErrorWithArgs(t *testing.T) { + // Fixture Setup + resourceName := "account-address" + + rootCmd := &cobra.Command{} + NewUpdateCommand(rootCmd) + updateCmd := getCommandForResource(rootCmd.Commands()[0], resourceName) + + // Execute SUT + err := updateCmd.Args(updateCmd, []string{"foo", "bar"}) + + // Verification + require.NoError(t, err) +} diff --git a/docs/runbook-development.md b/docs/runbook-development.md index 8638bb0..772cbb8 100644 --- a/docs/runbook-development.md +++ b/docs/runbook-development.md @@ -177,12 +177,5 @@ actions: epcc delete customer email=lindsey.sexton@test.example ``` -## Limitations - -Runbooks at the present time have the following limitations: - -1. The commands that are being run do not accept any flags. - - diff --git a/external/aliases/aliases.go b/external/aliases/aliases.go index cb5c867..47b0986 100644 --- a/external/aliases/aliases.go +++ b/external/aliases/aliases.go @@ -80,7 +80,7 @@ func getAliasesForSingleJsonApiType(jsonApiType string) map[string]*id.IdableAtt data, err := os.ReadFile(aliasFile) if err != nil { - log.Debugf("Could not read alias file: %s, error %s", aliasFile, err) + log.Tracef("Could not read alias file: %s, error %s", aliasFile, err) data = []byte{} } else { } diff --git a/external/autofill/autofill.go b/external/autofill/autofill.go index 82e2c09..c9dc16d 100644 --- a/external/autofill/autofill.go +++ b/external/autofill/autofill.go @@ -64,7 +64,7 @@ func GetJsonArrayForResource(r *resources.Resource) []string { if _, err := strconv.Atoi(arg); err == nil { // If we get an integer value back, lets just quote it. - arg = fmt.Sprintf("\"%s\"", v) + arg = fmt.Sprintf("\"%s\"", arg) } } diff --git a/external/httpclient/httpclient.go b/external/httpclient/httpclient.go index b4bccfb..3f5b72d 100644 --- a/external/httpclient/httpclient.go +++ b/external/httpclient/httpclient.go @@ -86,6 +86,11 @@ func init() { var Limit *rate.Limiter = nil +func Initialize(rateLimit uint16, requestTimeout float32) { + Limit = rate.NewLimiter(rate.Limit(rateLimit), 1) + HttpClient.Timeout = time.Duration(int64(requestTimeout*1000) * int64(time.Millisecond)) +} + var Retry429 = false var Retry5xx = false diff --git a/external/json/print_json.go b/external/json/print_json.go index 1982354..91e39c8 100644 --- a/external/json/print_json.go +++ b/external/json/print_json.go @@ -1,6 +1,7 @@ package json import ( + "bytes" gojson "encoding/json" "github.com/mattn/go-isatty" log "github.com/sirupsen/logrus" @@ -22,6 +23,15 @@ func PrintJsonToStderr(json string) error { return printJsonToWriter(json, os.Stderr) } +func PrettyPrint(in string) string { + var out bytes.Buffer + err := gojson.Indent(&out, []byte(in), "", " ") + if err != nil { + return in + } + return out.String() +} + func printJsonToWriter(json string, w io.Writer) error { // Adapted from gojq if os.Getenv("TERM") == "dumb" { diff --git a/external/resources/uritemplates.go b/external/resources/uritemplates.go index badb52f..f31edac 100644 --- a/external/resources/uritemplates.go +++ b/external/resources/uritemplates.go @@ -27,7 +27,7 @@ func GenerateUrlViaIdableAttributes(urlInfo *CrudEntityInfo, args []id.IdableAtt values := uritemplate.Values{} for idx, varName := range vars { - resourceType := convertUriTemplateValueToType(varName) + resourceType := ConvertUriTemplateValueToType(varName) _, ok := GetResourceByName(resourceType) if ok { attribute := "id" @@ -83,7 +83,7 @@ func GenerateUrl(urlInfo *CrudEntityInfo, args []string) (string, error) { values := uritemplate.Values{} for idx, varName := range vars { - resourceType := convertUriTemplateValueToType(varName) + resourceType := ConvertUriTemplateValueToType(varName) varType, ok := GetResourceByName(resourceType) if ok { attribute := "id" @@ -132,13 +132,35 @@ func GetTypesOfVariablesNeeded(url string) ([]string, error) { for _, value := range template.Varnames() { - results = append(results, convertUriTemplateValueToType(value)) + results = append(results, ConvertUriTemplateValueToType(value)) } return results, nil } -func convertUriTemplateValueToType(value string) string { +func GetSingularTypesOfVariablesNeeded(url string) ([]string, error) { + var ret []string + types, err := GetTypesOfVariablesNeeded(url) + + if err != nil { + return nil, err + } + + for _, t := range types { + + otherType, ok := GetResourceByName(t) + + if !ok { + log.Warnf("Error processing resource, could not find type %s", t) + } + + ret = append(ret, otherType.SingularName) + } + + return ret, nil +} + +func ConvertUriTemplateValueToType(value string) string { // URI templates must use _, so let's swap them for - return strings.ReplaceAll(value, "_", "-") } diff --git a/external/resources/yaml/resources.yaml b/external/resources/yaml/resources.yaml index 8e61f66..bbf978f 100644 --- a/external/resources/yaml/resources.yaml +++ b/external/resources/yaml/resources.yaml @@ -73,6 +73,14 @@ account-memberships: attributes: account_member_id: type: RESOURCE_ID:account-members +unassigned-account-memberships: + singular-name: "unassigned-account-membership" + json-api-type: "account_member" + json-api-format: "legacy" + docs: "https://elasticpath.dev/docs/accounts/using-account-membership-api/get-all-unassigned-account-members" + get-collection: + docs: "https://elasticpath.dev/docs/accounts/using-account-membership-api/get-all-unassigned-account-members" + url: "/v2/accounts/{accounts}/account-memberships/unassigned-account-members/" accounts: singular-name: "account" json-api-type: "account" @@ -1720,7 +1728,7 @@ pcm-pricebooks: attributes: name: type: STRING - descirption: + description: type: STRING pcm-product-prices: singular-name: "pcm-product-price" @@ -1738,7 +1746,7 @@ pcm-product-prices: url: "/pcm/pricebooks/{pcm_pricebooks}/prices/{pcm_product_prices}" create-entity: docs: "https://documentation.elasticpath.com/commerce-cloud/docs/api/pcm/pricebooks/prices/create-product-prices.html" - url: "pcm/pricebooks/{pcm_pricebooks}/prices" + url: "/pcm/pricebooks/{pcm_pricebooks}/prices" content-type: application/json update-entity: docs: "https://documentation.elasticpath.com/commerce-cloud/docs/api/pcm/pricebooks/prices/update-product-prices.html" diff --git a/external/resources/yaml/resources_yaml_test.go b/external/resources/yaml/resources_yaml_test.go index 63487e3..e052af1 100644 --- a/external/resources/yaml/resources_yaml_test.go +++ b/external/resources/yaml/resources_yaml_test.go @@ -8,7 +8,6 @@ import ( log "github.com/sirupsen/logrus" "github.com/yosida95/uritemplate/v3" "gopkg.in/yaml.v3" - "net/http" "os" "regexp" "strings" @@ -165,43 +164,43 @@ func TestJsonSchemaValidate(t *testing.T) { } } -func TestResourceDocsExist(t *testing.T) { - const httpStatusCodeOk = 200 - - Resources := resources.GetPluralResources() - linksReferenceCount := make(map[string]int, len(Resources)) - - for resource := range Resources { - linksReferenceCount[Resources[resource].Docs]++ - if Resources[resource].GetCollectionInfo != nil { - linksReferenceCount[Resources[resource].GetCollectionInfo.Docs]++ - } - if Resources[resource].CreateEntityInfo != nil { - linksReferenceCount[Resources[resource].CreateEntityInfo.Docs]++ - } - if Resources[resource].GetEntityInfo != nil { - linksReferenceCount[Resources[resource].GetEntityInfo.Docs]++ - } - if Resources[resource].UpdateEntityInfo != nil { - linksReferenceCount[Resources[resource].UpdateEntityInfo.Docs]++ - } - if Resources[resource].DeleteEntityInfo != nil { - linksReferenceCount[Resources[resource].DeleteEntityInfo.Docs]++ - } - } - - for link := range linksReferenceCount { - response, err := http.DefaultClient.Head(link) - if err != nil { - t.Errorf("Error Retrieving Link\nLink: %s\nError Message: %s\nReference Count: %d", link, err, linksReferenceCount[link]) - } else { - if response.StatusCode != httpStatusCodeOk { - t.Errorf("Unexpected Response\nLink: %s\nExpected Status Code: %d\nActual Status Code: %d\nReference Count: %d", - link, httpStatusCodeOk, response.StatusCode, linksReferenceCount[link]) - } - if err := response.Body.Close(); err != nil { - t.Errorf("Error Closing Reponse Body\nError Message: %s", err) - } - } - } -} +//func TestResourceDocsExist(t *testing.T) { +// const httpStatusCodeOk = 200 +// +// Resources := resources.GetPluralResources() +// linksReferenceCount := make(map[string]int, len(Resources)) +// +// for resource := range Resources { +// linksReferenceCount[Resources[resource].Docs]++ +// if Resources[resource].GetCollectionInfo != nil { +// linksReferenceCount[Resources[resource].GetCollectionInfo.Docs]++ +// } +// if Resources[resource].CreateEntityInfo != nil { +// linksReferenceCount[Resources[resource].CreateEntityInfo.Docs]++ +// } +// if Resources[resource].GetEntityInfo != nil { +// linksReferenceCount[Resources[resource].GetEntityInfo.Docs]++ +// } +// if Resources[resource].UpdateEntityInfo != nil { +// linksReferenceCount[Resources[resource].UpdateEntityInfo.Docs]++ +// } +// if Resources[resource].DeleteEntityInfo != nil { +// linksReferenceCount[Resources[resource].DeleteEntityInfo.Docs]++ +// } +// } +// +// for link := range linksReferenceCount { +// response, err := http.DefaultClient.Head(link) +// if err != nil { +// t.Errorf("Error Retrieving Link\nLink: %s\nError Message: %s\nReference Count: %d", link, err, linksReferenceCount[link]) +// } else { +// if response.StatusCode != httpStatusCodeOk { +// t.Errorf("Unexpected Response\nLink: %s\nExpected Status Code: %d\nActual Status Code: %d\nReference Count: %d", +// link, httpStatusCodeOk, response.StatusCode, linksReferenceCount[link]) +// } +// if err := response.Body.Close(); err != nil { +// t.Errorf("Error Closing Reponse Body\nError Message: %s", err) +// } +// } +// } +//} diff --git a/go.mod b/go.mod index 2eabae6..15d89e3 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,8 @@ require ( require github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 +require github.com/iancoleman/strcase v0.2.0 + require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.2.0 // indirect diff --git a/go.sum b/go.sum index 341285a..508a879 100644 --- a/go.sum +++ b/go.sum @@ -61,6 +61,8 @@ github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= +github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= diff --git a/main.go b/main.go index e8c57fa..6aba1d2 100644 --- a/main.go +++ b/main.go @@ -5,5 +5,6 @@ import ( ) func main() { + cmd.InitializeCmd() cmd.Execute() }