Skip to content

Commit 0d825d2

Browse files
author
Bruno Albuquerque
committed
Implement several useful interfaces.
- Implement JSON Marshaler/Unmarshaler. - Implement Text Marshaler/Unmarshaler. - Implement SQL Driver Valuer. - Implement SQL Scanner. - Change Value() to ID() to avoid clashes with SQL Driver Valuer.
1 parent 71cb30c commit 0d825d2

File tree

3 files changed

+151
-41
lines changed

3 files changed

+151
-41
lines changed

enum.go

Lines changed: 121 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,34 @@
11
package enum
22

33
import (
4+
"database/sql"
5+
"database/sql/driver"
6+
"encoding"
7+
"encoding/json"
8+
"fmt"
49
"reflect"
510

611
"golang.org/x/exp/constraints"
712
)
813

9-
// Enum represents a named Enum that is associaterd with a value. Enum values
10-
// are auto-generated starting form 0 and monotonically increasing in
14+
// Enum represents a named Enum that is associaterd with an ID. Enum IDs
15+
// are auto-generated starting from 0 and monotonically increasing in
1116
// declaration order.
1217
type Enum[T constraints.Integer] interface {
1318
Name() string
14-
Value() T
19+
ID() T
20+
21+
// JSON support.
22+
json.Marshaler
23+
json.Unmarshaler
24+
25+
// Text support.
26+
encoding.TextMarshaler
27+
encoding.TextUnmarshaler
28+
29+
// SQL support.
30+
driver.Valuer
31+
sql.Scanner
1532

1633
// Makes sure no external package can implement this interface.
1734
unimplementable()
@@ -31,7 +48,7 @@ func getTypeName[T any]() string {
3148
return tType.PkgPath() + "." + tType.Name()
3249
}
3350

34-
// New returns a new Enum associated with the given name and type T.
51+
// New returns a new Enum associated with the given name and type T.
3552
func New[T constraints.Integer](name string) Enum[T] {
3653
typeName := getTypeName[T]()
3754

@@ -48,20 +65,113 @@ func New[T constraints.Integer](name string) Enum[T] {
4865
}
4966

5067
// internalEnum is the only concrete implementation of the Enum interface. It
51-
// holds the name and typed value.
52-
type internalEnum[T comparable] struct {
53-
name string
54-
value T
68+
// holds the name and typed ID.
69+
type internalEnum[T constraints.Integer] struct {
70+
name string
71+
id T
5572
}
5673

5774
// Name returns the name associated with this Enum instance.
5875
func (e internalEnum[T]) Name() string {
5976
return e.name
6077
}
6178

62-
// Value returns the value associated with this Enum instance.
63-
func (e internalEnum[T]) Value() T {
64-
return e.value
79+
// ID returns the numeric ID associated with this Enum instance.
80+
func (e internalEnum[T]) ID() T {
81+
return e.id
82+
}
83+
84+
// MarshalJSON implements the json.Marshaler interface.
85+
func (e internalEnum[T]) MarshalJSON() ([]byte, error) {
86+
return json.Marshal(e.Name())
87+
}
88+
89+
func getIDForName[T constraints.Integer](name string) (T, error) {
90+
typeName := getTypeName[T]()
91+
92+
var defaultID T
93+
94+
anySet, ok := setByTypeName[typeName]
95+
if !ok {
96+
return defaultID, fmt.Errorf("no enum set associated with type %s", typeName)
97+
}
98+
99+
s := anySet.(*internalSet[T])
100+
101+
id, ok := s.ids[name]
102+
if !ok {
103+
return defaultID, fmt.Errorf("name %s could not be found in enum set for type %s", name, typeName)
104+
}
105+
106+
return T(id), nil
107+
}
108+
109+
// UnmarshalJSON implements the json.Unmarshaler interface.
110+
func (e *internalEnum[T]) UnmarshalJSON(data []byte) error {
111+
var name string
112+
if err := json.Unmarshal(data, &name); err != nil {
113+
return fmt.Errorf("source should be a string, got %s", data)
114+
}
115+
116+
id, err := getIDForName[T](name)
117+
if err != nil {
118+
return err
119+
}
120+
121+
e.name = name
122+
e.id = T(id)
123+
124+
return nil
125+
}
126+
127+
// MarshalText implements the encoding.TextMarshaler interface.
128+
func (e internalEnum[T]) MarshalText() ([]byte, error) {
129+
return []byte(e.Name()), nil
130+
}
131+
132+
// UnmarshalText implements the encoding.TextUnmarshaler interface.
133+
func (e *internalEnum[T]) UnmarshalText(text []byte) error {
134+
name := string(text)
135+
136+
id, err := getIDForName[T](name)
137+
if err != nil {
138+
return err
139+
}
140+
141+
e.name = name
142+
e.id = T(id)
143+
144+
return nil
145+
}
146+
147+
func (e internalEnum[T]) Value() (driver.Value, error) {
148+
return e.Name(), nil
149+
}
150+
151+
func (e *internalEnum[T]) Scan(value any) error {
152+
if value == nil {
153+
return nil
154+
}
155+
156+
name, ok := value.(string)
157+
if !ok {
158+
bytes, ok := value.([]byte)
159+
if !ok {
160+
return fmt.Errorf("value is not a byte slice")
161+
}
162+
163+
name = string(bytes[:])
164+
}
165+
166+
id, err := getIDForName[T](name)
167+
if err != nil {
168+
return err
169+
}
170+
171+
e.name = name
172+
e.id = T(id)
173+
174+
return nil
65175
}
66176

67177
// unimplementanle makes sure we implement the Enum interface.

enum_test.go

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,16 @@ func acceptsRoleOnly(t *testing.T, role Enum[Role]) {
2525
t.Log(role)
2626
}
2727

28-
func acceptsRoleValueOnly(t *testing.T, value Role) {
29-
t.Log(value)
28+
func acceptsRoleIDOnly(t *testing.T, id Role) {
29+
t.Log(id)
3030
}
3131

3232
func acceptsPermissionOnly(t *testing.T, permission Enum[Permission]) {
3333
t.Log(permission)
3434
}
3535

36-
func acceptsPermissionValueOnly(t *testing.T, value Permission) {
37-
t.Log(value)
36+
func acceptsPermissionIDOnly(t *testing.T, id Permission) {
37+
t.Log(id)
3838
}
3939

4040
func TestEnum(t *testing.T) {
@@ -45,22 +45,22 @@ func TestEnum(t *testing.T) {
4545

4646
// acceptsRoleOnly(t, UnknownPermission) // compile error
4747

48-
acceptsRoleValueOnly(t, UnknownRole.Value())
49-
acceptsRoleValueOnly(t, Admin.Value())
50-
acceptsRoleValueOnly(t, User.Value())
51-
acceptsRoleValueOnly(t, Guest.Value())
48+
acceptsRoleIDOnly(t, UnknownRole.ID())
49+
acceptsRoleIDOnly(t, Admin.ID())
50+
acceptsRoleIDOnly(t, User.ID())
51+
acceptsRoleIDOnly(t, Guest.ID())
5252

53-
// acceptsRoleValueOnly(t, UnknownPermission.Value()) // compile error
53+
// acceptsRoleIDOnly(t, UnknownPermission.ID()) // compile error
5454

5555
acceptsPermissionOnly(t, UnknownPermission)
5656
acceptsPermissionOnly(t, Read)
5757
acceptsPermissionOnly(t, Write)
5858

5959
// acceptsPermissionOnly(t, UnknownRole) // compile error
6060

61-
acceptsPermissionValueOnly(t, UnknownPermission.Value())
62-
acceptsPermissionValueOnly(t, Read.Value())
63-
acceptsPermissionValueOnly(t, Write.Value())
61+
acceptsPermissionIDOnly(t, UnknownPermission.ID())
62+
acceptsPermissionIDOnly(t, Read.ID())
63+
acceptsPermissionIDOnly(t, Write.ID())
6464

65-
// acceptsPermissionValueOnly(t, UnknownRole.Value()) // compile error
65+
// acceptsPermissionIDOnly(t, UnknownRole.ID()) // compile error
6666
}

set.go

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import (
99

1010
// internalSet collects all enums associated with a specific type T.
1111
type internalSet[T constraints.Integer] struct {
12-
nextValue int64 // Atomically updated.
13-
values map[string]int64
12+
nextID int64 // Atomically updated.
13+
ids map[string]int64
1414
}
1515

1616
// newInternalSet returns a new empty set.
@@ -21,43 +21,43 @@ func newInternalSet[T constraints.Integer]() *internalSet[T] {
2121
}
2222
}
2323

24-
// Add adds a new enum with the given name to the set. The enum value is
24+
// Add adds a new enum with the given name to the set. The enum ID is
2525
// auto-generated based on the instantiation order of enums. This panics if
2626
// an attempt is made to add an enum with a name that already exists in the
2727
// set.
2828
func (s *internalSet[T]) Add(name string) Enum[T] {
29-
if _, ok := s.values[name]; ok {
29+
if _, ok := s.ids[name]; ok {
3030
panic("duplicate name in set")
3131
}
3232

33-
value := atomic.AddInt64(&s.nextValue, 1)
34-
value--
33+
id := atomic.AddInt64(&s.nextID, 1)
34+
id--
3535

3636
// TODO(bga): Check for integer overflow as T might be any integer type
3737
// (say, int8).
3838

39-
s.values[name] = value
39+
s.ids[name] = id
4040

41-
return internalEnum[T]{name: name, value: T(value)}
41+
return &internalEnum[T]{name: name, id: T(id)}
4242
}
4343

4444
// GetByName returns the Enum associated with the given name and type T.
45-
func (s internalSet[T]) GetByName(name string) (Enum[T], error) {
46-
value, ok := s.values[name]
45+
func (s *internalSet[T]) GetByName(name string) (Enum[T], error) {
46+
id, ok := s.ids[name]
4747
if !ok {
4848
return nil, fmt.Errorf("name %s could not be found in set", name)
4949
}
5050

51-
return internalEnum[T]{name: name, value: T(value)}, nil
51+
return &internalEnum[T]{name: name, id: T(id)}, nil
5252
}
5353

54-
// GetByValue returns the Enum associated with the given value and type T.
55-
func (s internalSet[T]) GetByValue(value int64) (Enum[T], error) {
56-
for name, v := range s.values {
57-
if v == value {
58-
return internalEnum[T]{name: name, value: T(value)}, nil
54+
// GetByID returns the Enum associated with the given ID and type T.
55+
func (s *internalSet[T]) GetByID(id T) (Enum[T], error) {
56+
for name, v := range s.ids {
57+
if v == int64(id) {
58+
return &internalEnum[T]{name: name, id: T(id)}, nil
5959
}
6060
}
6161

62-
return nil, fmt.Errorf("value %d could not be found in set", value)
62+
return nil, fmt.Errorf("id %d could not be found in set", id)
6363
}

0 commit comments

Comments
 (0)