refactor: Move from global state to functions (#53)

This commit represents a few experiments of features I've used in Cobra

1. Uses cli.GenericFlag to encapsulate parsing and validation of flag
   values at parse time. This removes the burden from the individual
   CLI commands to parse and validate args and options.

2. Add influxid.ID that may be used by any flag that requires an
   Influx ID. influxid.ID parses and validates string value is a valid
   ID, removing this burden from individual commands and ensuring valid
   values before the command actions begins.

3. Binds cli.Flags directly to params structures to directly capture
   the values when parsing flags.

4. Moves from global state to local builder functions for the majority
   of the commands. This allows the commands to bind to flag variables
   reducing the repeated ctx.String(), ctx.Int(), etc

5. Leverages the BeforeFunc to create middleware and inject the CLI and
   API client into commands, saving the repeated boilerplate across
   all of the instantiated commands. This is extensible, so additional
   middleware can be appends using the middleware.WithBeforeFns
This commit is contained in:
Stuart Carnie
2021-05-03 23:31:45 +10:00
committed by GitHub
parent 0b4d753728
commit 3414e1a983
13 changed files with 1058 additions and 569 deletions

View File

@ -0,0 +1,20 @@
package middleware
import (
"github.com/urfave/cli/v2"
)
// WithBeforeFns returns a cli.BeforeFunc that calls each of the provided
// functions in order.
// NOTE: The first function to return an error will end execution and
// be returned as the error value of the composed function.
func WithBeforeFns(fns ...cli.BeforeFunc) cli.BeforeFunc {
return func(ctx *cli.Context) error {
for _, fn := range fns {
if err := fn(ctx); err != nil {
return err
}
}
return nil
}
}

132
pkg/influxid/id.go Normal file
View File

@ -0,0 +1,132 @@
package influxid
import (
"encoding/binary"
"encoding/hex"
"errors"
"strconv"
)
// IDLength is the exact length a string (or a byte slice representing it) must have in order to be decoded into a valid ID.
const IDLength = 16
var (
// ErrInvalidID signifies invalid IDs.
ErrInvalidID = errors.New("invalid ID")
// ErrInvalidIDLength is returned when an ID has the incorrect number of bytes.
ErrInvalidIDLength = errors.New("id must have a length of 16 bytes")
)
// ID is a unique identifier.
//
// Its zero value is not a valid ID.
type ID uint64
// IDFromString creates an ID from a given string.
//
// It errors if the input string does not match a valid ID.
func IDFromString(str string) (ID, error) {
var id ID
err := id.DecodeFromString(str)
if err != nil {
return 0, err
}
return id, nil
}
// MustIDFromString is like IDFromString but panics if
// s is not a valid base-16 identifier.
func MustIDFromString(s string) ID {
id, err := IDFromString(s)
if err != nil {
panic(err)
}
return id
}
// Decode parses b as a hex-encoded byte-slice-string.
//
// It errors if the input byte slice does not have the correct length
// or if it contains all zeros.
func (i *ID) Decode(b []byte) error {
if len(b) != IDLength {
return ErrInvalidIDLength
}
res, err := strconv.ParseUint(string(b), 16, 64)
if err != nil {
return ErrInvalidID
}
if *i = ID(res); !i.Valid() {
return ErrInvalidID
}
return nil
}
// DecodeFromString parses s as a hex-encoded string.
func (i *ID) DecodeFromString(s string) error {
return i.Decode([]byte(s))
}
// Encode converts ID to a hex-encoded byte-slice-string.
//
// It errors if the receiving ID holds its zero value.
func (i ID) Encode() ([]byte, error) {
if !i.Valid() {
return nil, ErrInvalidID
}
b := make([]byte, hex.DecodedLen(IDLength))
binary.BigEndian.PutUint64(b, uint64(i))
dst := make([]byte, hex.EncodedLen(len(b)))
hex.Encode(dst, b)
return dst, nil
}
// Valid checks whether the receiving ID is a valid one or not.
func (i ID) Valid() bool {
return i != 0
}
// String returns the ID as a hex encoded string.
//
// Returns an empty string in the case the ID is invalid.
func (i ID) String() string {
enc, _ := i.Encode()
return string(enc)
}
// GoString formats the ID the same as the String method.
// Without this, when using the %#v verb, an ID would be printed as a uint64,
// so you would see e.g. 0x2def021097c6000 instead of 02def021097c6000
// (note the leading 0x, which means the former doesn't show up in searches for the latter).
func (i ID) GoString() string {
return `"` + i.String() + `"`
}
// MarshalText encodes i as text.
// Providing this method is a fallback for json.Marshal,
// with the added benefit that IDs encoded as map keys will be the expected string encoding,
// rather than the effective fmt.Sprintf("%d", i) that json.Marshal uses by default for integer types.
func (i ID) MarshalText() ([]byte, error) {
return i.Encode()
}
// UnmarshalText decodes i from a byte slice.
// Providing this method is also a fallback for json.Unmarshal,
// also relevant when IDs are used as map keys.
func (i *ID) UnmarshalText(b []byte) error {
return i.Decode(b)
}
func (i *ID) Set(s string) error {
id, err := IDFromString(s)
if err != nil {
return err
}
*i = id
return nil
}

223
pkg/influxid/id_test.go Normal file
View File

@ -0,0 +1,223 @@
package influxid_test
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"testing"
"github.com/influxdata/influx-cli/v2/pkg/influxid"
)
func TestIDFromString(t *testing.T) {
tests := []struct {
name string
id string
want influxid.ID
wantErr bool
err string
}{
{
name: "Should be able to decode an all zeros ID",
id: "0000000000000000",
wantErr: true,
err: influxid.ErrInvalidID.Error(),
},
{
name: "Should be able to decode an all f ID",
id: "ffffffffffffffff",
want: influxid.MustIDFromString("ffffffffffffffff"),
},
{
name: "Should be able to decode an ID",
id: "020f755c3c082000",
want: influxid.MustIDFromString("020f755c3c082000"),
},
{
name: "Should not be able to decode a non hex ID",
id: "gggggggggggggggg",
wantErr: true,
err: influxid.ErrInvalidID.Error(),
},
{
name: "Should not be able to decode inputs with length less than 16 bytes",
id: "abc",
wantErr: true,
err: influxid.ErrInvalidIDLength.Error(),
},
{
name: "Should not be able to decode inputs with length greater than 16 bytes",
id: "abcdabcdabcdabcd0",
wantErr: true,
err: influxid.ErrInvalidIDLength.Error(),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := influxid.IDFromString(tt.id)
// Check negative test cases
if (err != nil) && tt.wantErr {
if tt.err != err.Error() {
t.Errorf("IDFromString() errors out \"%s\", want \"%s\"", err, tt.err)
}
return
}
// Check positive test cases
if !reflect.DeepEqual(got, tt.want) && !tt.wantErr {
t.Errorf("IDFromString() outputs %v, want %v", got, tt.want)
}
})
}
}
func TestDecodeFromString(t *testing.T) {
var id influxid.ID
err := id.DecodeFromString("020f755c3c082000")
if err != nil {
t.Errorf(err.Error())
}
want := []byte{48, 50, 48, 102, 55, 53, 53, 99, 51, 99, 48, 56, 50, 48, 48, 48}
got, _ := id.Encode()
if !bytes.Equal(want, got) {
t.Errorf("got %s not equal to wanted %s", string(got), string(want))
}
if id.String() != "020f755c3c082000" {
t.Errorf("expecting string representation to contain the right value")
}
if !id.Valid() {
t.Errorf("expecting ID to be a valid one")
}
}
func TestEncode(t *testing.T) {
var id influxid.ID
if _, err := id.Encode(); err == nil {
t.Errorf("encoding an invalid ID should not be possible")
}
id.DecodeFromString("5ca1ab1eba5eba11")
want := []byte{53, 99, 97, 49, 97, 98, 49, 101, 98, 97, 53, 101, 98, 97, 49, 49}
got, _ := id.Encode()
if !bytes.Equal(want, got) {
t.Errorf("encoding error")
}
if id.String() != "5ca1ab1eba5eba11" {
t.Errorf("expecting string representation to contain the right value")
}
if !id.Valid() {
t.Errorf("expecting ID to be a valid one")
}
}
func TestDecodeFromAllZeros(t *testing.T) {
var id influxid.ID
err := id.Decode(make([]byte, influxid.IDLength))
if err == nil {
t.Errorf("expecting all zeros ID to not be a valid ID")
}
}
func TestDecodeFromShorterString(t *testing.T) {
var id influxid.ID
err := id.DecodeFromString("020f75")
if err == nil {
t.Errorf("expecting shorter inputs to error")
}
if id.String() != "" {
t.Errorf("expecting invalid ID to be serialized into empty string")
}
}
func TestDecodeFromLongerString(t *testing.T) {
var id influxid.ID
err := id.DecodeFromString("020f755c3c082000aaa")
if err == nil {
t.Errorf("expecting shorter inputs to error")
}
if id.String() != "" {
t.Errorf("expecting invalid ID to be serialized into empty string")
}
}
func TestDecodeFromEmptyString(t *testing.T) {
var id influxid.ID
err := id.DecodeFromString("")
if err == nil {
t.Errorf("expecting empty inputs to error")
}
if id.String() != "" {
t.Errorf("expecting invalid ID to be serialized into empty string")
}
}
func TestMarshalling(t *testing.T) {
var id0 influxid.ID
_, err := json.Marshal(id0)
if err == nil {
t.Errorf("expecting empty ID to not be a valid one")
}
init := "ca55e77eca55e77e"
id1, err := influxid.IDFromString(init)
if err != nil {
t.Errorf(err.Error())
}
serialized, err := json.Marshal(id1)
if err != nil {
t.Errorf(err.Error())
}
var id2 influxid.ID
json.Unmarshal(serialized, &id2)
bytes1, _ := id1.Encode()
bytes2, _ := id2.Encode()
if !bytes.Equal(bytes1, bytes2) {
t.Errorf("error marshalling/unmarshalling ID")
}
// When used as a map key, IDs must use their string encoding.
// If you only implement json.Marshaller, they will be encoded with Go's default integer encoding.
b, err := json.Marshal(map[influxid.ID]int{0x1234: 5678})
if err != nil {
t.Error(err)
}
const exp = `{"0000000000001234":5678}`
if string(b) != exp {
t.Errorf("expected map to json.Marshal as %s; got %s", exp, string(b))
}
var idMap map[influxid.ID]int
if err := json.Unmarshal(b, &idMap); err != nil {
t.Error(err)
}
if len(idMap) != 1 {
t.Errorf("expected length 1, got %d", len(idMap))
}
if idMap[0x1234] != 5678 {
t.Errorf("unmarshalled incorrectly; exp 0x1234:5678, got %v", idMap)
}
}
func TestID_GoString(t *testing.T) {
type idGoStringTester struct {
ID influxid.ID
}
var x idGoStringTester
const idString = "02def021097c6000"
if err := x.ID.DecodeFromString(idString); err != nil {
t.Fatal(err)
}
sharpV := fmt.Sprintf("%#v", x)
want := `influxid_test.idGoStringTester{ID:"` + idString + `"}`
if sharpV != want {
t.Fatalf("bad GoString: got %q, want %q", sharpV, want)
}
}