From a952dff92dc457bf48df240b0ed1c36ca70fb9b8 Mon Sep 17 00:00:00 2001
From: Dan Moran <dmoran@influxdata.com>
Date: Tue, 24 Nov 2020 10:13:50 -0500
Subject: [PATCH] feat(kit/cli): add support for int32 and int64 CLI flags

---
 kit/cli/viper.go      | 52 +++++++++++++++++++++++++++++++++++++++++++
 kit/cli/viper_test.go | 35 +++++++++++++++++++++++++++--
 2 files changed, 85 insertions(+), 2 deletions(-)

diff --git a/kit/cli/viper.go b/kit/cli/viper.go
index 225877885d..118a2c8300 100644
--- a/kit/cli/viper.go
+++ b/kit/cli/viper.go
@@ -150,6 +150,58 @@ func BindOptions(v *viper.Viper, cmd *cobra.Command, opts []Opt) {
 			}
 			mustBindPFlag(v, o.Flag, flagset)
 			*destP = v.GetInt(envVar)
+		case *int32:
+			var d int32
+			if o.Default != nil {
+				// N.B. since our CLI kit types default values as interface{} and
+				// literal numbers get typed as int by default, it's very easy to
+				// create an int32 CLI flag with an int default value.
+				//
+				// The compiler doesn't know to complain in that case, so you end up
+				// with a runtime panic when trying to bind the CLI options.
+				//
+				// To avoid that headache, we support both int32 and int defaults
+				// for int32 fields. This introduces a new runtime bomb if somebody
+				// specifies an int default > math.MaxInt32, but that's hopefully
+				// less likely.
+				var ok bool
+				d, ok = o.Default.(int32)
+				if !ok {
+					d = int32(o.Default.(int))
+				}
+			}
+			if hasShort {
+				flagset.Int32VarP(destP, o.Flag, string(o.Short), d, o.Desc)
+			} else {
+				flagset.Int32Var(destP, o.Flag, d, o.Desc)
+			}
+			mustBindPFlag(v, o.Flag, flagset)
+			*destP = v.GetInt32(envVar)
+		case *int64:
+			var d int64
+			if o.Default != nil {
+				// N.B. since our CLI kit types default values as interface{} and
+				// literal numbers get typed as int by default, it's very easy to
+				// create an int64 CLI flag with an int default value.
+				//
+				// The compiler doesn't know to complain in that case, so you end up
+				// with a runtime panic when trying to bind the CLI options.
+				//
+				// To avoid that headache, we support both int64 and int defaults
+				// for int64 fields.
+				var ok bool
+				d, ok = o.Default.(int64)
+				if !ok {
+					d = int64(o.Default.(int))
+				}
+			}
+			if hasShort {
+				flagset.Int64VarP(destP, o.Flag, string(o.Short), d, o.Desc)
+			} else {
+				flagset.Int64Var(destP, o.Flag, d, o.Desc)
+			}
+			mustBindPFlag(v, o.Flag, flagset)
+			*destP = v.GetInt64(envVar)
 		case *bool:
 			var d bool
 			if o.Default != nil {
diff --git a/kit/cli/viper_test.go b/kit/cli/viper_test.go
index 28b5010fad..440ae0b425 100644
--- a/kit/cli/viper_test.go
+++ b/kit/cli/viper_test.go
@@ -4,6 +4,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"io/ioutil"
+	"math"
 	"os"
 	"path"
 	"testing"
@@ -40,6 +41,8 @@ func (c *customFlag) Type() string {
 func ExampleNewCommand() {
 	var monitorHost string
 	var number int
+	var smallerNumber int32
+	var longerNumber int64
 	var sleep bool
 	var duration time.Duration
 	var stringSlice []string
@@ -50,6 +53,7 @@ func ExampleNewCommand() {
 			for i := 0; i < number; i++ {
 				fmt.Printf("%d\n", i)
 			}
+			fmt.Println(longerNumber - int64(smallerNumber))
 			fmt.Println(sleep)
 			fmt.Println(duration)
 			fmt.Println(stringSlice)
@@ -70,6 +74,18 @@ func ExampleNewCommand() {
 				Default: 2,
 				Desc:    "number of times to loop",
 			},
+			{
+				DestP:   &smallerNumber,
+				Flag:    "smaller-number",
+				Default: math.MaxInt32,
+				Desc:    "limited size number",
+			},
+			{
+				DestP:   &longerNumber,
+				Flag:    "longer-number",
+				Default: math.MaxInt64,
+				Desc:    "explicitly expanded-size number",
+			},
 			{
 				DestP:   &sleep,
 				Flag:    "sleep",
@@ -104,6 +120,7 @@ func ExampleNewCommand() {
 	// http://localhost:8086
 	// 0
 	// 1
+	// 9223372034707292160
 	// true
 	// 1m0s
 	// [foo bar]
@@ -113,8 +130,10 @@ func ExampleNewCommand() {
 func Test_NewProgram(t *testing.T) {
 	testFilePath, cleanup := newConfigFile(t, map[string]string{
 		// config values should be same as flags
-		"foo":      "bar",
-		"shoe-fly": "yadon",
+		"foo":         "bar",
+		"shoe-fly":    "yadon",
+		"number":      "2147483647",
+		"long-number": "9223372036854775807",
 	})
 	defer cleanup()
 	defer setEnvVar("TEST_CONFIG_PATH", testFilePath)()
@@ -155,6 +174,8 @@ func Test_NewProgram(t *testing.T) {
 
 			var testVar string
 			var testFly string
+			var testNumber int32
+			var testLongNumber int64
 			program := &Program{
 				Name: "test",
 				Opts: []Opt{
@@ -167,6 +188,14 @@ func Test_NewProgram(t *testing.T) {
 						DestP: &testFly,
 						Flag:  "shoe-fly",
 					},
+					{
+						DestP: &testNumber,
+						Flag:  "number",
+					},
+					{
+						DestP: &testLongNumber,
+						Flag:  "long-number",
+					},
 				},
 				Run: func() error { return nil },
 			}
@@ -177,6 +206,8 @@ func Test_NewProgram(t *testing.T) {
 
 			require.Equal(t, tt.expected, testVar)
 			assert.Equal(t, "yadon", testFly)
+			assert.Equal(t, int32(math.MaxInt32), testNumber)
+			assert.Equal(t, int64(math.MaxInt64), testLongNumber)
 		}
 
 		t.Run(tt.name, fn)