Skip to content

Commit 7985830

Browse files
feat(autoflags): Add AutoFlags
1 parent ff652a8 commit 7985830

4 files changed

Lines changed: 342 additions & 0 deletions

File tree

yaautoflags/constants.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package yaautoflags
2+
3+
const (
4+
flagsFieldName = "Flags"
5+
bitsInByte = 8
6+
)

yaautoflags/errors.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package yaautoflags
2+
3+
import (
4+
"errors"
5+
)
6+
7+
var (
8+
ErrInstanceNil = errors.New("instance cannot be nil")
9+
ErrInstanceNotStruct = errors.New("instance must be a struct")
10+
ErrFlagsFieldNotFound = errors.New("flags field not found in struct")
11+
ErrFlagsFieldTypeMismatch = errors.New("flags field must be of type uint64, uint32, uint16, uint8, uint or uintptr")
12+
ErrTooManyFlags = errors.New("too many flags set")
13+
)

yaautoflags/yaautoflags.go

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
package yaautoflags
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"reflect"
7+
8+
"github.com/YaCodeDev/GoYaCodeDevUtils/yaerrors"
9+
)
10+
11+
// PackFlags packs boolean fields of a struct into a single flags field.
12+
// The struct must have a field named "Flags" of type uint64, uint32, uint16, uint8, uint or uintptr.
13+
// The boolean fields are packed into the flags field, where each bit represents a boolean field.
14+
// The first boolean field corresponds to the least significant bit of the flags field.
15+
// If the number of boolean fields exceeds the size of the flags field, an error is returned.
16+
// The flags field must be of a type that can hold the number of boolean fields.
17+
// If the flags field is not found or is of an incorrect type, an error is returned.
18+
//
19+
// Example usage:
20+
//
21+
// type MyFlags struct {
22+
// A, B, C bool
23+
// Flags uint8 // or uint64, uint32, uint16, uint, uintptr
24+
// }
25+
//
26+
// err := PackFlags(&MyFlags{A: true, B: false, C: true})
27+
//
28+
// if err != nil {
29+
// // handle error
30+
// }
31+
func PackFlags[T any](instance *T) yaerrors.Error {
32+
if instance == nil {
33+
return yaerrors.FromError(
34+
http.StatusInternalServerError,
35+
ErrInstanceNil,
36+
"pack flags",
37+
)
38+
}
39+
40+
reflectValue := reflect.ValueOf(instance).Elem()
41+
reflectType := reflectValue.Type()
42+
43+
if reflectValue.Kind() != reflect.Struct {
44+
return yaerrors.FromError(
45+
http.StatusInternalServerError,
46+
ErrInstanceNotStruct,
47+
"pack flags",
48+
)
49+
}
50+
51+
var flagsField reflect.Value
52+
53+
var nextFlagIndex uint8
54+
55+
var flags uint64
56+
57+
var flagsSize uint8
58+
59+
for i := range reflectValue.NumField() {
60+
fieldValue := reflectValue.Field(i)
61+
fieldType := reflectType.Field(i)
62+
63+
if fieldValue.Kind() == reflect.Bool {
64+
if fieldValue.Bool() {
65+
flags |= 1 << nextFlagIndex
66+
}
67+
68+
nextFlagIndex++
69+
}
70+
71+
if fieldType.Name == flagsFieldName {
72+
flagsField = fieldValue
73+
flagsSize = uint8(flagsField.Type().Size() * bitsInByte)
74+
75+
// nolint: exhaustive
76+
switch flagsField.Kind() {
77+
case reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8, reflect.Uint, reflect.Uintptr:
78+
default:
79+
return yaerrors.FromError(
80+
http.StatusInternalServerError,
81+
ErrFlagsFieldTypeMismatch,
82+
"pack flags: got "+flagsField.Kind().String(),
83+
)
84+
}
85+
}
86+
}
87+
88+
if flagsSize == 0 {
89+
return yaerrors.FromError(
90+
http.StatusInternalServerError,
91+
ErrFlagsFieldNotFound,
92+
"pack flags",
93+
)
94+
}
95+
96+
if nextFlagIndex >= flagsSize {
97+
return yaerrors.FromError(
98+
http.StatusInternalServerError,
99+
ErrFlagsFieldNotFound,
100+
fmt.Sprintf(
101+
"pack flags: maximum is %d for %s",
102+
flagsSize,
103+
flagsField.Kind().String(),
104+
),
105+
)
106+
}
107+
108+
flagsField.SetUint(flags)
109+
110+
return nil
111+
}
112+
113+
// UnpackFlags unpacks a flags field into boolean fields of a struct.
114+
// The struct must have a field named "Flags" of type uint64, uint32, uint16, uint8, uint or uintptr.
115+
// The boolean fields are unpacked from the flags field, where each bit represents a boolean field.
116+
// The first boolean field corresponds to the least significant bit of the flags field.
117+
// If the number of boolean fields exceeds the size of the flags field, an error is returned.
118+
// The flags field must be of a type that can hold the number of boolean fields.
119+
// If the flags field is not found or is of an incorrect type, an error is returned.
120+
// Example usage:
121+
//
122+
// type MyFlags struct {
123+
// A, B, C bool
124+
// Flags uint8 // or uint64, uint32, uint16, uint, uintptr
125+
// }
126+
//
127+
// err := UnpackFlags(&MyFlags{Flags: 0b101})
128+
//
129+
// err != nil {
130+
// // handle error
131+
// }
132+
func UnpackFlags[T any](instance *T) yaerrors.Error {
133+
if instance == nil {
134+
return yaerrors.FromError(
135+
http.StatusInternalServerError,
136+
ErrInstanceNil,
137+
"unpack flags",
138+
)
139+
}
140+
141+
reflectValue := reflect.ValueOf(instance).Elem()
142+
143+
if reflectValue.Kind() != reflect.Struct {
144+
return yaerrors.FromError(
145+
http.StatusInternalServerError,
146+
ErrInstanceNotStruct,
147+
"unpack flags",
148+
)
149+
}
150+
151+
flagsField := reflectValue.FieldByName(flagsFieldName)
152+
153+
if !flagsField.IsValid() {
154+
return yaerrors.FromError(
155+
http.StatusInternalServerError,
156+
ErrFlagsFieldNotFound,
157+
"unpack flags",
158+
)
159+
}
160+
161+
// nolint: exhaustive
162+
switch flagsField.Kind() {
163+
case reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8, reflect.Uint, reflect.Uintptr:
164+
default:
165+
return yaerrors.FromError(
166+
http.StatusInternalServerError,
167+
ErrFlagsFieldTypeMismatch,
168+
"unpack flags: got "+flagsField.Kind().String(),
169+
)
170+
}
171+
172+
flags := flagsField.Uint()
173+
174+
flagsSize := uint8(flagsField.Type().Size() * bitsInByte)
175+
176+
var nextFlagIndex uint8
177+
178+
for i := range reflectValue.NumField() {
179+
fieldValue := reflectValue.Field(i)
180+
181+
if fieldValue.Kind() != reflect.Bool {
182+
continue
183+
}
184+
185+
if nextFlagIndex >= flagsSize {
186+
return yaerrors.FromError(
187+
http.StatusInternalServerError,
188+
ErrTooManyFlags,
189+
fmt.Sprintf(
190+
"unpack flags: maximum is %d for %s",
191+
flagsSize,
192+
flagsField.Kind().String(),
193+
),
194+
)
195+
}
196+
197+
val := (flags>>nextFlagIndex)&1 == 1
198+
fieldValue.SetBool(val)
199+
200+
nextFlagIndex++
201+
}
202+
203+
return nil
204+
}

yaautoflags/yaautoflags_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package yaautoflags
2+
3+
import (
4+
"errors"
5+
"reflect"
6+
"testing"
7+
)
8+
9+
func assertErrIs(t *testing.T, got error, want error) {
10+
t.Helper()
11+
12+
if !errors.Is(got, want) {
13+
t.Fatalf("expected error %v, got %v", want, got)
14+
}
15+
}
16+
17+
func assertNoErr(t *testing.T, got error) {
18+
t.Helper()
19+
20+
if got != nil {
21+
t.Fatalf("unexpected error: %v", got)
22+
}
23+
}
24+
25+
type noFlags struct {
26+
B0 bool
27+
}
28+
29+
type wrongFlagsType struct {
30+
B0 bool
31+
Flags int64
32+
}
33+
34+
type justEnough struct {
35+
B0, B1, B2, B3 bool
36+
Flags uint8
37+
}
38+
39+
type overflowUint8 struct {
40+
B0, B1, B2, B3, B4, B5, B6, B7, B8 bool
41+
Flags uint8
42+
}
43+
44+
func TestPackFlags_NilInstance(t *testing.T) {
45+
var s *justEnough // typed nil ptr
46+
assertErrIs(t, PackFlags(s), ErrInstanceNil)
47+
}
48+
49+
func TestPackFlags_NotStruct(t *testing.T) {
50+
v := 7
51+
assertErrIs(t, PackFlags(&v), ErrInstanceNotStruct)
52+
}
53+
54+
func TestPackFlags_FlagsFieldNotFound(t *testing.T) {
55+
assertErrIs(t, PackFlags(&noFlags{B0: true}), ErrFlagsFieldNotFound)
56+
}
57+
58+
func TestPackFlags_FlagsFieldWrongType(t *testing.T) {
59+
assertErrIs(t, PackFlags(&wrongFlagsType{B0: true}), ErrFlagsFieldTypeMismatch)
60+
}
61+
62+
func TestPackFlags_TooManyBoolsForUint8(t *testing.T) {
63+
inst := &overflowUint8{B0: true, B8: true}
64+
assertErrIs(t, PackFlags(inst), ErrFlagsFieldNotFound)
65+
}
66+
67+
func TestPackFlags_HappyPath(t *testing.T) {
68+
inst := &justEnough{B0: true, B1: false, B2: true, B3: true}
69+
assertNoErr(t, PackFlags(inst))
70+
71+
const want uint8 = 0b1101
72+
if inst.Flags != want {
73+
t.Fatalf("expected Flags=%08b, got %08b", want, inst.Flags)
74+
}
75+
}
76+
77+
func TestUnpackFlags_NilInstance(t *testing.T) {
78+
var s *justEnough
79+
assertErrIs(t, UnpackFlags(s), ErrInstanceNil)
80+
}
81+
82+
func TestUnpackFlags_NotStruct(t *testing.T) {
83+
v := 42
84+
assertErrIs(t, UnpackFlags(&v), ErrInstanceNotStruct)
85+
}
86+
87+
func TestUnpackFlags_FlagsFieldNotFound(t *testing.T) {
88+
assertErrIs(t, UnpackFlags(&noFlags{}), ErrFlagsFieldNotFound)
89+
}
90+
91+
func TestUnpackFlags_FlagsFieldWrongType(t *testing.T) {
92+
assertErrIs(t, UnpackFlags(&wrongFlagsType{Flags: 1}), ErrFlagsFieldTypeMismatch)
93+
}
94+
95+
func TestUnpackFlags_TooManyFlagsToUnpack(t *testing.T) {
96+
assertErrIs(t, UnpackFlags(&overflowUint8{Flags: 0xFF}), ErrTooManyFlags)
97+
}
98+
99+
func TestUnpackFlags_HappyPath(t *testing.T) {
100+
src := &justEnough{Flags: 0b1010}
101+
assertNoErr(t, UnpackFlags(src))
102+
103+
want := &justEnough{B1: true, B3: true, Flags: 0b1010}
104+
if !reflect.DeepEqual(src, want) {
105+
t.Fatalf("unpack failed: want %+v, got %+v", want, src)
106+
}
107+
}
108+
109+
func TestRoundTrip_PackThenUnpack(t *testing.T) {
110+
orig := &justEnough{B0: true, B1: true, B2: false, B3: true}
111+
assertNoErr(t, PackFlags(orig))
112+
113+
cloned := &justEnough{Flags: orig.Flags}
114+
assertNoErr(t, UnpackFlags(cloned))
115+
116+
if !reflect.DeepEqual(orig, cloned) {
117+
t.Fatalf("round-trip mismatch: want %+v, got %+v", orig, cloned)
118+
}
119+
}

0 commit comments

Comments
 (0)