diff --git a/fs/bits.go b/fs/bits.go new file mode 100644 index 000000000..ed2ac6338 --- /dev/null +++ b/fs/bits.go @@ -0,0 +1,148 @@ +package fs + +import ( + "encoding/json" + "fmt" + "strings" +) + +// Bits is an option which can be any combination of the Choices. +// +// Suggested implementation is something like this: +// +// type bits = Bits[bitsChoices] +// +// const ( +// bitA bits = 1 << iota +// bitB +// bitC +// ) +// +// type bitsChoices struct{} +// +// func (bitsChoices) Choices() []BitsChoicesInfo { +// return []BitsChoicesInfo{ +// {uint64(0), "OFF"}, // Optional Off value - "" if not defined +// {uint64(bitA), "A"}, +// {uint64(bitB), "B"}, +// {uint64(bitC), "C"}, +// } +// } +type Bits[C BitsChoices] uint64 + +// BitsChoicesInfo should be returned from the Choices method +type BitsChoicesInfo struct { + Bit uint64 + Name string +} + +// BitsChoices returns the valid choices for this type. +// +// It must work on the zero value. +// +// Note that when using this in an Option the ExampleBitsChoices will be +// filled in automatically. +type BitsChoices interface { + // Choices returns the valid choices for each bit of this type + Choices() []BitsChoicesInfo +} + +// String turns a Bits into a string +func (b Bits[C]) String() string { + var out []string + choices := b.Choices() + // Return an off value if set + if b == 0 { + for _, info := range choices { + if info.Bit == 0 { + return info.Name + } + } + } + for _, info := range choices { + if info.Bit == 0 { + continue + } + if b&Bits[C](info.Bit) != 0 { + out = append(out, info.Name) + b &^= Bits[C](info.Bit) + } + } + if b != 0 { + out = append(out, fmt.Sprintf("Unknown-0x%X", int(b))) + } + return strings.Join(out, ",") +} + +// Help returns a comma separated list of all possible bits. +func (b Bits[C]) Help() string { + var out []string + for _, info := range b.Choices() { + out = append(out, info.Name) + } + return strings.Join(out, ", ") +} + +// Choices returns the possible values of the Bits. +func (b Bits[C]) Choices() []BitsChoicesInfo { + var c C + return c.Choices() +} + +// Set a Bits as a comma separated list of flags +func (b *Bits[C]) Set(s string) error { + var flags Bits[C] + parts := strings.Split(s, ",") + choices := b.Choices() + for _, part := range parts { + found := false + part = strings.TrimSpace(part) + if part == "" { + continue + } + for _, info := range choices { + if strings.EqualFold(info.Name, part) { + found = true + flags |= Bits[C](info.Bit) + } + } + if !found { + return fmt.Errorf("invalid choice %q from: %s", part, b.Help()) + } + } + *b = flags + return nil +} + +// Type of the value. +// +// If C has a Type() string method then it will be used instead. +func (b Bits[C]) Type() string { + var c C + if do, ok := any(c).(typer); ok { + return do.Type() + } + return "Bits" +} + +// Scan implements the fmt.Scanner interface +func (b *Bits[C]) Scan(s fmt.ScanState, ch rune) error { + token, err := s.Token(true, nil) + if err != nil { + return err + } + return b.Set(string(token)) +} + +// UnmarshalJSON makes sure the value can be parsed as a string or integer in JSON +func (b *Bits[C]) UnmarshalJSON(in []byte) error { + return UnmarshalJSONFlag(in, b, func(i int64) error { + *b = (Bits[C])(i) + return nil + }) +} + +// MarshalJSON encodes it as string +func (b *Bits[C]) MarshalJSON() ([]byte, error) { + return json.Marshal(b.String()) +} diff --git a/fs/bits_test.go b/fs/bits_test.go new file mode 100644 index 000000000..6f6c55504 --- /dev/null +++ b/fs/bits_test.go @@ -0,0 +1,142 @@ +package fs + +import ( + "encoding/json" + "fmt" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type bits = Bits[bitsChoices] + +const ( + bitA bits = 1 << iota + bitB + bitC +) + +type bitsChoices struct{} + +func (bitsChoices) Choices() []BitsChoicesInfo { + return []BitsChoicesInfo{ + {uint64(0), "OFF"}, + {uint64(bitA), "A"}, + {uint64(bitB), "B"}, + {uint64(bitC), "C"}, + } +} + +// Check it satisfies the interfaces +var ( + _ flagger = (*bits)(nil) + _ flaggerNP = bits(0) +) + +func TestBitsString(t *testing.T) { + assert.Equal(t, "OFF", bits(0).String()) + assert.Equal(t, "A", (bitA).String()) + assert.Equal(t, "A,B", (bitA | bitB).String()) + assert.Equal(t, "A,B,C", (bitA | bitB | bitC).String()) + assert.Equal(t, "A,Unknown-0x8000", (bitA | bits(0x8000)).String()) +} + +func TestBitsHelp(t *testing.T) { + assert.Equal(t, "OFF, A, B, C", bits(0).Help()) +} + +func TestBitsSet(t *testing.T) { + for _, test := range []struct { + in string + want bits + wantErr string + }{ + {"", bits(0), ""}, + {"B", bitB, ""}, + {"B,A", bitB | bitA, ""}, + {"a,b,C", bitA | bitB | bitC, ""}, + {"A,B,unknown,E", 0, `invalid choice "unknown" from: OFF, A, B, C`}, + } { + f := bits(0xffffffffffffffff) + initial := f + err := f.Set(test.in) + if err != nil { + if test.wantErr == "" { + t.Errorf("Got an error when not expecting one on %q: %v", test.in, err) + } else { + assert.Contains(t, err.Error(), test.wantErr) + } + assert.Equal(t, initial, f, test.want) + } else { + if test.wantErr != "" { + t.Errorf("Got no error when expecting one on %q", test.in) + } else { + assert.Equal(t, test.want, f) + } + } + + } +} + +func TestBitsType(t *testing.T) { + f := bits(0) + assert.Equal(t, "Bits", f.Type()) +} + +func TestBitsScan(t *testing.T) { + var v bits + n, err := fmt.Sscan(" C,B ", &v) + require.NoError(t, err) + assert.Equal(t, 1, n) + assert.Equal(t, bitC|bitB, v) +} + +func TestBitsUnmarshallJSON(t *testing.T) { + for _, test := range []struct { + in string + want bits + wantErr string + }{ + {`""`, bits(0), ""}, + {`"B"`, bitB, ""}, + {`"B,A"`, bitB | bitA, ""}, + {`"A,B,C"`, bitA | bitB | bitC, ""}, + {`"A,B,unknown,E"`, 0, `invalid choice "unknown" from: OFF, A, B, C`}, + {`0`, bits(0), ""}, + {strconv.Itoa(int(bitB)), bitB, ""}, + {strconv.Itoa(int(bitB | bitA)), bitB | bitA, ""}, + } { + f := bits(0xffffffffffffffff) + initial := f + err := json.Unmarshal([]byte(test.in), &f) + if err != nil { + if test.wantErr == "" { + t.Errorf("Got an error when not expecting one on %q: %v", test.in, err) + } else { + assert.Contains(t, err.Error(), test.wantErr) + } + assert.Equal(t, initial, f, test.want) + } else { + if test.wantErr != "" { + t.Errorf("Got no error when expecting one on %q", test.in) + } else { + assert.Equal(t, test.want, f) + } + } + } +} +func TestBitsMarshalJSON(t *testing.T) { + for _, test := range []struct { + in bits + want string + }{ + {bitA | bitC, `"A,C"`}, + {0, `"OFF"`}, + } { + got, err := json.Marshal(&test.in) + require.NoError(t, err) + assert.Equal(t, test.want, string(got), fmt.Sprintf("%#v", test.in)) + } +}