fs: create fs.Bits for easy creation of parameters from a bitset of choices
This commit is contained in:
parent
1cc22da87d
commit
75745fcb21
2 changed files with 290 additions and 0 deletions
148
fs/bits.go
Normal file
148
fs/bits.go
Normal file
|
@ -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())
|
||||||
|
}
|
142
fs/bits_test.go
Normal file
142
fs/bits_test.go
Normal file
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue