diff --git a/.gitignore b/.gitignore index 7bb114d..fe8246b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,6 @@ *.dll *.so *.dylib -dynamomq # Test binary, built with `go test -c` *.test diff --git a/cmd/dynamomq/main.go b/cmd/dynamomq/main.go index 6f4a0a7..a570492 100644 --- a/cmd/dynamomq/main.go +++ b/cmd/dynamomq/main.go @@ -1,112 +1,9 @@ package main import ( - "bufio" - "context" - "flag" - "fmt" - "os" - "strings" - - "github.com/aws/aws-sdk-go-v2/config" - "github.com/vvatanabe/dynamomq" - "github.com/vvatanabe/dynamomq/internal/cli" + "github.com/vvatanabe/dynamomq/internal/cmd" ) func main() { - defer fmt.Printf("... CLI is ending\n\n\n") - - fmt.Println("===========================================================") - fmt.Println(">> Welcome to Priority Queueing CLI Tool!") - fmt.Println("===========================================================") - fmt.Println("for help, enter one of the following: ? or h or help") - fmt.Println("all commands in CLIs need to be typed in lowercase") - fmt.Println("") - - executionPath, _ := os.Getwd() - tableName := flag.String("table", dynamomq.DefaultTableName, "AWS DynamoDB table name") - endpoint := flag.String("endpoint-url", "", "AWS DynamoDB base endpoint url") - - flag.Parse() - - cfg, err := config.LoadDefaultConfig(context.Background()) - if err != nil { - fmt.Printf("failed to load aws config: %s\n", err) - os.Exit(1) - } - - fmt.Printf("current dir is: [%s]\n", executionPath) - fmt.Printf("region is: [%s]\n", cfg.Region) - fmt.Printf("table is: [%s]\n", *tableName) - fmt.Printf("endpoint is: [%s]\n", *endpoint) - fmt.Println("") - - client, err := dynamomq.NewFromConfig[any](cfg, - dynamomq.WithTableName(*tableName), - dynamomq.WithAWSBaseEndpoint(*endpoint)) - if err != nil { - fmt.Printf("... AWS session could not be established!: %v\n", err) - } else { - fmt.Println("... AWS session is properly established!") - } - - c := cli.CLI{ - TableName: *tableName, - Client: client, - Message: nil, - } - - // 1. Create a Scanner using the InputStream available. - scanner := bufio.NewScanner(os.Stdin) - - for { - // 2. Don't forget to prompt the user - if c.Message != nil { - fmt.Printf("\nID <%s> >> Enter command: ", c.Message.ID) - } else { - fmt.Print("\n>> Enter command: ") - } - - // 3. Use the Scanner to read a line of text from the user. - scanned := scanner.Scan() - if !scanned { - break - } - - input := scanner.Text() - if input == "" { - continue - } - - command, params := parseInput(input) - switch command { - case "": - continue - case "quit", "q": - return - default: - // 4. Now, you can do anything with the input string that you need to. - // Like, output it to the user. - c.Run(context.Background(), command, params) - } - } -} - -func parseInput(input string) (command string, params []string) { - input = strings.TrimSpace(input) - arr := strings.Fields(input) - - if len(arr) == 0 { - return "", nil - } - - command = strings.ToLower(arr[0]) - - if len(arr) > 1 { - params = make([]string, len(arr)-1) - for i := 1; i < len(arr); i++ { - params[i-1] = strings.TrimSpace(arr[i]) - } - } - return command, params + cmd.Execute() } diff --git a/go.mod b/go.mod index 22f6f97..2894277 100644 --- a/go.mod +++ b/go.mod @@ -5,10 +5,10 @@ go 1.21.0 require ( github.com/aws/aws-sdk-go-v2 v1.21.0 github.com/aws/aws-sdk-go-v2/config v1.18.42 - github.com/aws/aws-sdk-go-v2/credentials v1.13.40 github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.10.39 github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.4.66 github.com/aws/aws-sdk-go-v2/service/dynamodb v1.21.5 + github.com/spf13/cobra v1.7.0 github.com/upsidr/dynamotest v0.1.1 ) @@ -16,6 +16,7 @@ require ( github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.13.40 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35 // indirect @@ -37,6 +38,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/imdario/mergo v0.3.16 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/term v0.5.0 // indirect @@ -46,6 +48,7 @@ require ( github.com/ory/dockertest/v3 v3.10.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/sirupsen/logrus v1.9.3 // indirect + github.com/spf13/pflag v1.0.5 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect diff --git a/go.sum b/go.sum index b8c7597..b0898b7 100644 --- a/go.sum +++ b/go.sum @@ -44,6 +44,7 @@ github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqy github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/containerd/continuity v0.4.2 h1:v3y/4Yz5jwnvqPKJJ+7Wf93fyWoCB3F5EclWG023MDM= github.com/containerd/continuity v0.4.2/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -67,6 +68,8 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= @@ -91,8 +94,13 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go deleted file mode 100644 index 7f1e458..0000000 --- a/internal/cli/cli_test.go +++ /dev/null @@ -1 +0,0 @@ -package cli diff --git a/internal/cli/cli.go b/internal/cmd/root.go similarity index 64% rename from internal/cli/cli.go rename to internal/cmd/root.go index 534ad06..8580738 100644 --- a/internal/cli/cli.go +++ b/internal/cmd/root.go @@ -1,9 +1,15 @@ -package cli +package cmd import ( + "bufio" "context" "encoding/json" "fmt" + "os" + "strings" + + "github.com/aws/aws-sdk-go-v2/config" + "github.com/spf13/cobra" "github.com/vvatanabe/dynamomq" @@ -13,17 +19,142 @@ import ( "github.com/vvatanabe/dynamomq/internal/test" ) +var flgs = &Flags{} + +var rootCmd = &cobra.Command{ + Use: "dynamomq", + Short: "dynamomq is a tool for implementing message queueing with Amazon DynamoDB in Go", + Long: `dynamomq is a tool for implementing message queueing with Amazon DynamoDB in Go. + +Environment Variables: + * AWS_REGION + * AWS_PROFILE + * AWS_ACCESS_KEY_ID + * AWS_SECRET_ACCESS_KEY + * AWS_SESSION_TOKEN + refs: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html +`, + Version: "", + RunE: func(cmd *cobra.Command, args []string) error { + defer fmt.Printf("... Interactive is ending\n\n\n") + + fmt.Println("===========================================================") + fmt.Println(">> Welcome to DynamoMQ CLI! [INTERACTIVE MODE]") + fmt.Println("===========================================================") + fmt.Println("for help, enter one of the following: ? or h or help") + fmt.Println("all commands in CLIs need to be typed in lowercase") + fmt.Println("") + + ctx := context.Background() + cfg, err := config.LoadDefaultConfig(ctx) + if err != nil { + return fmt.Errorf("Failed to load aws config: %s\n", err) + } + + fmt.Printf("AWSRegion: %s\n", cfg.Region) + fmt.Printf("TableName: %s\n", flgs.TableName) + fmt.Printf("EndpointURL: %s\n", flgs.EndpointURL) + fmt.Println("") + + client, err := dynamomq.NewFromConfig[any](cfg, + dynamomq.WithTableName(flgs.TableName), + dynamomq.WithAWSBaseEndpoint(flgs.EndpointURL)) + if err != nil { + return fmt.Errorf("... AWS session could not be established!: %v\n", err) + } + fmt.Println("... AWS session is properly established!") + + c := Interactive{ + TableName: flgs.TableName, + Client: client, + Message: nil, + } + + // 1. Create a Scanner using the InputStream available. + scanner := bufio.NewScanner(os.Stdin) + + for { + // 2. Don't forget to prompt the user + if c.Message != nil { + fmt.Printf("\nID <%s> >> Enter command: ", c.Message.ID) + } else { + fmt.Print("\n>> Enter command: ") + } + + // 3. Use the Scanner to read a line of text from the user. + scanned := scanner.Scan() + if !scanned { + break + } + + input := scanner.Text() + if input == "" { + continue + } + + command, params := parseInput(input) + switch command { + case "": + continue + case "quit", "q": + return nil + default: + // 4. Now, you can do anything with the input string that you need to. + // Like, output it to the user. + c.Run(context.Background(), command, params) + } + } + return nil + }, +} + +func parseInput(input string) (command string, params []string) { + input = strings.TrimSpace(input) + arr := strings.Fields(input) + + if len(arr) == 0 { + return "", nil + } + + command = strings.ToLower(arr[0]) + + if len(arr) > 1 { + params = make([]string, len(arr)-1) + for i := 1; i < len(arr); i++ { + params[i-1] = strings.TrimSpace(arr[i]) + } + } + return command, params +} + +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +type Flags struct { + TableName string + EndpointURL string +} + +func init() { + rootCmd.Flags().StringVar(&flgs.TableName, "table-name", dynamomq.DefaultTableName, "The name of the table to contain the item.") + rootCmd.Flags().StringVar(&flgs.EndpointURL, "endpoint-url", "", "Override command's default URL with the given URL.") +} + const ( needAWSMessage = "Need first to run 'aws' command" ) -type CLI struct { +type Interactive struct { TableName string Client dynamomq.Client[any] Message *dynamomq.Message[any] } -func (c *CLI) Run(ctx context.Context, command string, params []string) { +func (c *Interactive) Run(ctx context.Context, command string, params []string) { switch command { case "h", "?", "help": c.help(ctx, params) @@ -62,15 +193,15 @@ func (c *CLI) Run(ctx context.Context, command string, params []string) { } } -func (c *CLI) help(_ context.Context, _ []string) { - fmt.Println(`... this is CLI HELP! +func (c *Interactive) help(_ context.Context, _ []string) { + fmt.Println(`... this is Interactive HELP! > qstat | qstats [Retrieves the Queue statistics (no need to be in App mode)] > dlq [Retrieves the Dead Letter Queue (DLQ) statistics] > enqueue-test | et [SendMessage test Message records in DynamoDB: A-101, A-202, A-303 and A-404; if already exists, it will overwrite it] > purge [It will remove all test data from DynamoDB] > ls [ListMessages all message IDs ... max 10 elements] > receive [ReceiveMessage the Message from the Queue .. it will replace the current ID with the peeked one] - > id [GetMessage the application object from DynamoDB by app domain ID; CLI is in the app mode, from that point on] + > id [GetMessage the application object from DynamoDB by app domain ID; Interactive is in the app mode, from that point on] > sys [Show system info data in a JSON format] > data [Print the data as JSON for the current message record] > info [Print all info regarding Message record: system_info and data as JSON] @@ -82,18 +213,14 @@ func (c *CLI) help(_ context.Context, _ []string) { > id`) } -func (c *CLI) ls(ctx context.Context, _ []string) { - if c.Client == nil { - fmt.Println(needAWSMessage) - return - } +func (c *Interactive) ls(ctx context.Context, _ []string) { out, err := c.Client.ListMessages(ctx, &dynamomq.ListMessagesInput{Size: 10}) if err != nil { printError(err) return } if len(out.Messages) == 0 { - fmt.Println("Message table is empty!") + fmt.Println("Queue is empty!") return } fmt.Println("ListMessages of first 10 IDs:") @@ -102,11 +229,7 @@ func (c *CLI) ls(ctx context.Context, _ []string) { } } -func (c *CLI) purge(ctx context.Context, _ []string) { - if c.Client == nil { - fmt.Println(needAWSMessage) - return - } +func (c *Interactive) purge(ctx context.Context, _ []string) { out, err := c.Client.ListMessages(ctx, &dynamomq.ListMessagesInput{Size: 10}) if err != nil { printError(err) @@ -129,11 +252,7 @@ func (c *CLI) purge(ctx context.Context, _ []string) { } } -func (c *CLI) enqueueTest(ctx context.Context, _ []string) { - if c.Client == nil { - fmt.Println(needAWSMessage) - return - } +func (c *Interactive) enqueueTest(ctx context.Context, _ []string) { fmt.Println("SendMessage message with IDs:") ids := []string{"A-101", "A-202", "A-303", "A-404"} for _, id := range ids { @@ -162,11 +281,7 @@ func (c *CLI) enqueueTest(ctx context.Context, _ []string) { } } -func (c *CLI) qstat(ctx context.Context, _ []string) { - if c.Client == nil { - fmt.Println(needAWSMessage) - return - } +func (c *Interactive) qstat(ctx context.Context, _ []string) { stats, err := c.Client.GetQueueStats(ctx, &dynamomq.GetQueueStatsInput{}) if err != nil { printError(err) @@ -175,11 +290,7 @@ func (c *CLI) qstat(ctx context.Context, _ []string) { printMessageWithData("Queue status:\n", stats) } -func (c *CLI) dlq(ctx context.Context, _ []string) { - if c.Client == nil { - fmt.Println(needAWSMessage) - return - } +func (c *Interactive) dlq(ctx context.Context, _ []string) { stats, err := c.Client.GetDLQStats(ctx, &dynamomq.GetDLQStatsInput{}) if err != nil { printError(err) @@ -188,11 +299,7 @@ func (c *CLI) dlq(ctx context.Context, _ []string) { printMessageWithData("DLQ status:\n", stats) } -func (c *CLI) receive(ctx context.Context, _ []string) { - if c.Client == nil { - fmt.Println(needAWSMessage) - return - } +func (c *Interactive) receive(ctx context.Context, _ []string) { rr, err := c.Client.ReceiveMessage(ctx, &dynamomq.ReceiveMessageInput{}) if err != nil { printError(fmt.Sprintf("ReceiveMessage has failed! message: %s", err)) @@ -210,14 +317,10 @@ func (c *CLI) receive(ctx context.Context, _ []string) { printMessageWithData("Queue stats:\n", stats) } -func (c *CLI) id(ctx context.Context, params []string) { +func (c *Interactive) id(ctx context.Context, params []string) { if len(params) == 0 { c.Message = nil - fmt.Println("Going back to standard CLI mode!") - return - } - if c.Client == nil { - fmt.Println(needAWSMessage) + fmt.Println("Going back to standard Interactive mode!") return } id := params[0] @@ -237,11 +340,7 @@ func (c *CLI) id(ctx context.Context, params []string) { printMessageWithData(fmt.Sprintf("Message's [%s] record dump:\n", id), c.Message) } -func (c *CLI) system(_ context.Context, _ []string) { - if c.Client == nil { - fmt.Println(needAWSMessage) - return - } +func (c *Interactive) system(_ context.Context, _ []string) { if c.Message == nil { printCLIModeRestriction("`system` or `sys`") return @@ -249,11 +348,7 @@ func (c *CLI) system(_ context.Context, _ []string) { printMessageWithData("ID's system info:\n", c.Message.GetSystemInfo()) } -func (c *CLI) reset(ctx context.Context, _ []string) { - if c.Client == nil { - fmt.Println(needAWSMessage) - return - } +func (c *Interactive) reset(ctx context.Context, _ []string) { if c.Message == nil { printCLIModeRestriction("`reset`") return @@ -269,11 +364,7 @@ func (c *CLI) reset(ctx context.Context, _ []string) { printMessageWithData("Reset system info:\n", c.Message.GetSystemInfo()) } -func (c *CLI) redrive(ctx context.Context, _ []string) { - if c.Client == nil { - fmt.Println(needAWSMessage) - return - } +func (c *Interactive) redrive(ctx context.Context, _ []string) { if c.Message == nil { printCLIModeRestriction("`redrive`") return @@ -288,11 +379,7 @@ func (c *CLI) redrive(ctx context.Context, _ []string) { printMessageWithData("Ready system info:\n", result) } -func (c *CLI) delete(ctx context.Context, _ []string) { - if c.Client == nil { - fmt.Println(needAWSMessage) - return - } +func (c *Interactive) delete(ctx context.Context, _ []string) { if c.Message == nil { printCLIModeRestriction("`done`") return @@ -314,11 +401,7 @@ func (c *CLI) delete(ctx context.Context, _ []string) { printMessageWithData("Queue status:\n", stats) } -func (c *CLI) fail(ctx context.Context, _ []string) { - if c.Client == nil { - fmt.Println(needAWSMessage) - return - } +func (c *Interactive) fail(ctx context.Context, _ []string) { if c.Message == nil { printCLIModeRestriction("`fail`") return @@ -349,11 +432,7 @@ func (c *CLI) fail(ctx context.Context, _ []string) { printMessageWithData("Queue status:\n", stats) } -func (c *CLI) invalid(ctx context.Context, _ []string) { - if c.Client == nil { - fmt.Println(needAWSMessage) - return - } +func (c *Interactive) invalid(ctx context.Context, _ []string) { if c.Message == nil { printCLIModeRestriction("`invalid`") return @@ -374,11 +453,7 @@ func (c *CLI) invalid(ctx context.Context, _ []string) { printMessageWithData("Queue status:\n", stats) } -func (c *CLI) data(_ context.Context, _ []string) { - if c.Client == nil { - fmt.Println(needAWSMessage) - return - } +func (c *Interactive) data(_ context.Context, _ []string) { if c.Message == nil { printCLIModeRestriction("`data`") return @@ -386,11 +461,7 @@ func (c *CLI) data(_ context.Context, _ []string) { printMessageWithData("Data info:\n", c.Message.Data) } -func (c *CLI) info(_ context.Context, _ []string) { - if c.Client == nil { - fmt.Println(needAWSMessage) - return - } +func (c *Interactive) info(_ context.Context, _ []string) { if c.Message == nil { printCLIModeRestriction("`info`") return @@ -416,7 +487,7 @@ func marshalIndent(v any) ([]byte, error) { } func printCLIModeRestriction(command string) { - printError(fmt.Sprintf("%s command can be only used in the CLI's App mode. Call first `id ", command)) + printError(fmt.Sprintf("%s command can be only used in the Interactive's App mode. Call first `id ", command)) } func printError(err any) { diff --git a/internal/cmd/root_test.go b/internal/cmd/root_test.go new file mode 100644 index 0000000..1d619dd --- /dev/null +++ b/internal/cmd/root_test.go @@ -0,0 +1 @@ +package cmd