Skip to content

Commit

Permalink
support extend methods (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
huykingsofm committed Dec 10, 2024
1 parent 55794fa commit 5bff66a
Show file tree
Hide file tree
Showing 8 changed files with 215 additions and 82 deletions.
133 changes: 94 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,18 @@

**Elegant and powerful enums for Go with zero code generation!**

[1]: #-iota-enum
[1]: #-basic-enum
[2]: #-wrapenum
[3]: #-safeenum
[4]: #-utility-functions
[5]: #-constant-support
[6]: #-serialization-and-deserialization
[7]: #-type-safety

> [!WARNING]
> Please keep in mind that `xybor-x/enum` is still under active development
> and therefore full backward compatibility is not guaranteed before reaching v1.0.0.
## 🔧 Installation

```sh
Expand All @@ -27,43 +31,34 @@ go get -u github.com/xybor-x/enum

## 📋 Features

All enum types behave nearly consistently, so you can choose the style that best fits your use case without worrying about differences in functionality. You can refer to the [recommendations](#-recommendations).

| | Basic enum ([#][1]) | Wrap enum ([#][2]) | Safe enum ([#][3]) |
| -------------------------- | ------------------- | ------------------ | ------------------ |
| **Built-in methods** | No | Yes | Yes |
| **Constant enum** ([#][5]) | Yes | Yes | No |
| **Enum type** | Any integer types | `int` | `struct` |
| **Enum value type** | Any integer types | `int` | `struct` |
| **Serde** ([#][6]) | No | Yes | Yes |
| **Type safety** ([#][7]) | No | Basic | Strong |

**Note**: Enum definitions are ***NOT thread-safe***. Therefore, they should be finalized during initialization (at the global scope).
All of the following enum types are compatible with the APIs provided by `xybor-x/enum`.

| | Basic enum ([#][1]) | Wrap enum ([#][2]) | Safe enum ([#][3]) |
| ---------------------------------------------- | ------------------- | ------------------ | ------------------ |
| **Built-in methods** | No | **Yes** | **Yes** |
| **Constant enum** ([#][5]) | **Yes** | **Yes** | No |
| **Serialization and deserialization** ([#][6]) | No | **Yes** | **Yes** |
| **Type safety** ([#][7]) | No | Basic | **Strong** |

## 🔍 Recommendations

| | Basic enum | Wrap enum | Safe enum |
| ----------------------------- | ---------- | --------- | --------- |
| **Simplified use** | Yes | Yes | Yes |
| **Exhaustive check required** | Yes | Yes | No |
| **Type safety required** | No | Maybe | Yes |
> [!CAUTION]
> Enum definitions are not thread-safe.
> Therefore, they should be finalized during initialization (at the global scope).

## ⭐ Basic enum

The basic enum (`iota` approach) is the most commonly used enum implementation in Go.
The basic enum (a.k.a `iota` enum) is the most commonly used enum implementation in Go.

It is essentially a primitive type, which does not include any built-in methods. For handling this type of enum, please refer to the [utility functions][4].

**Pros 💪**
- Simple.
- Supports constant values.
- Supports constant values ([#][5]).

**Cons 👎**
- No built-in methods.
- No type safety ([#][7]).
- Lacks serialization and deserialization support.
- No type safety.

``` go
type Role int
Expand All @@ -81,22 +76,23 @@ func init() {
```

## ⭐ WrapEnum

`WrapEnum` offers a set of built-in methods to simplify working with enums.

**Pros 💪**
- Supports constant values.
- Supports constant values ([#][5]).
- Provides many useful built-in methods.
- Full serialization and deserialization support out of the box.

**Cons 👎**
- Provides only **basic type safety**.
- Provides only **basic type safety** ([#][7]).

```go
// Define enum's underlying type.
type underlyingRole any
type role any

// Create a WrapEnum type for roles.
type Role = enum.WrapEnum[underlyingRole] // NOTE: It must use type alias instead of type definition.
type Role = enum.WrapEnum[role] // NOTE: It must use type alias instead of type definition.

const (
RoleUser Role = iota
Expand Down Expand Up @@ -128,23 +124,23 @@ func main() {
The `SafeEnum` enforces strict type safety, ensuring that only predefined enum values are allowed. It prevents the accidental creation of new enum types, providing a guaranteed set of valid values.

**Pros 💪**
- Provides **strong type safety**.
- Provides **strong type safety** ([#][7]).
- Provides many useful built-in methods.
- Full serialization and deserialization support out of the box.

**Cons 👎**
- Does not support constant values.
- Does not support constant values ([#][5]).

```go
// Define enum's underlying type.
type underlyingRole string
type role any

// Create a StructEnum type for roles.
type Role = enum.SafeEnum[underlyingRole] // NOTE: It must use type alias instead of type definition.
// Create a SafeEnum type for roles.
type Role = enum.SafeEnum[role] // NOTE: It must use type alias instead of type definition.

var (
RoleUser = enum.NewSafe[underlyingRole]("user")
RoleAdmin = enum.NewSafe[underlyingRole]("admin")
RoleUser = enum.NewSafe[Role]("user")
RoleAdmin = enum.NewSafe[Role]("admin")
)

func main() {
Expand All @@ -156,7 +152,8 @@ func main() {

## 💡 Utility functions

*All of the following functions can be used with any style of enum. Note that this differs from the built-in methods, which are tied to the enum object rather than being standalone functions.*
> [!NOTE]
> All of the following functions can be used with any type of enum.
### FromString

Expand Down Expand Up @@ -233,25 +230,83 @@ Some static analysis tools support checking for exhaustive `switch` statements i

Serialization and deserialization are essential when working with enums, and our library provides seamless support for handling them out of the box.

> [!WARNING]
> Not all enum types support serde operations, please refer to the [features](#-features).
Currently supported:
- `JSON`: Implements `json.Marshaler` and `json.Unmarshaler`.
- `SQL`: Implements `driver.Valuer` and `sql.Scanner`.

*Note that NOT ALL enum styles support serde operations, please refer to the [features/serde](#-features).*

## 🔅 Type safety

`WrapEnum` includes built-in methods for serialization and deserialization, offering **basic type safety** and preventing most invalid enum cases.
The [WrapEnum][2] prevents most invalid enum cases due to built-in methods for serialization and deserialization, offering **basic type safety**.

However, it is still possible to accidentally create an invalid enum value, like this:

```go
moderator := Role(42) // Invalid enum value
```

The [`SafeEnum`][4] provides **strong type safety**, ensuring that only predefined enum values are allowed. There is no way to create a new `SafeEnum` object without explicitly using the `NewSafe` function or zero initialization.
The [SafeEnum][3] provides **strong type safety**, ensuring that only predefined enum values are allowed. There is no way to create a new `SafeEnum` object without explicitly using the `NewSafe` function or zero initialization.

```go
moderator := Role(42) // Compile-time error
moderator := Role("moderator") // Compile-time error
```

## 🔅 Extensible

### Extend basic enum

Since this enum is just a primitive type and does not have built-in methods, you can easily extend it by directly adding additional methods.

```go
type Role int

const (
RoleUser Role = iota
RoleMod
RoleAdmin
)

func init() {
enum.Map(RoleUser, "user")
enum.Map(RoleMod, "mod")
enum.Map(RoleAdmin, "admin")
enum.Finalize[Role]()
}

func (r Role) HasPermission() bool {
return r == RoleMod || r == RoleAdmin
}
```

### Extend WrapEnum

`WrapEnum` has many predefined methods. The only way to retain these methods while extending it is to wrap it as an embedded field in another struct.

However, this approach will break the constant-support property of the `WrapEnum` because Go does not support constants for structs.

You should consider extending [Basic enum](#extend-basic-enum) or [Safe enum](#extend-safeenum) instead.

### Extend SafeEnum

`SafeEnum` has many predefined methods. The only way to retain these methods while extending it is to wrap it as an embedded field in another struct.

`xybor-x/enum` provides the `NewExtendedSafe` function to help create a wrapper of SafeEnum.

```go
type role any
type Role struct { enum.SafeEnum[role] }

var (
RoleUser = enum.NewExtendedSafe[Role]("user")
RoleMod = enum.NewExtendedSafe[Role]("mod")
RoleAdmin = enum.NewExtendedSafe[Role]("admin")
_ = enum.Finalize[Role]()
)

func (r Role) HasPermission() bool {
return r == RoleMod || r == RoleAdmin
}
```
2 changes: 1 addition & 1 deletion enum.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import (
// need a constant enum, declare it explicitly and use enum.Map() instead.
// - This function is not thread-safe and should only be called during
// initialization or other safe execution points to avoid race conditions.
func New[T common.Integral](s string) T {
func New[T common.Integer](s string) T {
id := core.GetAvailableEnumValue[T]()
return core.MapAny(id, T(id), s)
}
Expand Down
32 changes: 1 addition & 31 deletions enum_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ func TestEnumJSON(t *testing.T) {
assert.Equal(t, RoleUser, s.Role)
}

func TestBasicEnumPrintZeroStruct(t *testing.T) {
func TestEnumPrintZeroStruct(t *testing.T) {
type Role int

var (
Expand All @@ -381,33 +381,3 @@ func TestBasicEnumPrintZeroStruct(t *testing.T) {

assert.Equal(t, "{0}", fmt.Sprint(User{}))
}

func TestWrapEnumPrintZeroStruct(t *testing.T) {
type underlyingRole any
type Role = enum.WrapEnum[underlyingRole]

var (
_ = enum.New[Role]("user")
)

type User struct {
Role Role
}

assert.Equal(t, "{user}", fmt.Sprint(User{}))
}

func TestStructEnumPrintZeroStruct(t *testing.T) {
type underlyingRole any
type Role = enum.SafeEnum[underlyingRole]

var (
_ = enum.NewSafe[underlyingRole]("user")
)

type User struct {
Role Role
}

assert.Equal(t, "{<nil>}", fmt.Sprint(User{}))
}
8 changes: 4 additions & 4 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,13 @@ func ExampleWrapEnum() {

func ExampleSafeEnum() {
// Define a generic enum type
type underlyingRole string
type Role = enum.SafeEnum[underlyingRole]
type role string
type Role = enum.SafeEnum[role]

// Define enum values for Role using iota
var (
RoleUser = enum.NewSafe[underlyingRole]("user")
RoleAdmin = enum.NewSafe[underlyingRole]("admin")
RoleUser = enum.NewSafe[Role]("user")
RoleAdmin = enum.NewSafe[Role]("admin")
_ = enum.Finalize[Role]() // Optional: ensure no new enum values can be added to Role.
)

Expand Down
2 changes: 1 addition & 1 deletion internal/common/integral.go → internal/common/integer.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package common

type Integral interface {
type Integer interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}
54 changes: 48 additions & 6 deletions safe_enum.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package enum
import (
"database/sql/driver"
"fmt"
"reflect"

"github.com/xybor-x/enum/internal/core"
)
Expand All @@ -18,17 +19,48 @@ type SafeEnum[underlyingEnum any] struct {
inner string
}

type safeEnumer interface {
newsafeenum(s string) any
}

// NewSafe creates a new StructEnum with its string representation. The library
// automatically assigns the smallest available number to the enum.
//
// Note that this function is not thread-safe and should only be called during
// initialization or other safe execution points to avoid race conditions.
func NewSafe[underlyingEnum any](inner string) SafeEnum[underlyingEnum] {
return core.MapAny(
core.GetAvailableEnumValue[SafeEnum[underlyingEnum]](),
SafeEnum[underlyingEnum]{inner: inner},
inner,
)
func NewSafe[T safeEnumer](inner string) T {
var defaultT T
return defaultT.newsafeenum(inner).(T)
}

// NewExtendedSafe helps to initialize the extended safe enum.
//
// Note that this function is not thread-safe and should only be called during
// initialization or other safe execution points to avoid race conditions.
func NewExtendedSafe[T safeEnumer](s string) T {
var t T

tvalue := reflect.ValueOf(&t).Elem()

for i := 0; i < tvalue.NumField(); i++ {
fieldType := reflect.TypeOf(t).Field(i)
if !fieldType.Anonymous {
continue
}

if !fieldType.Type.Implements(reflect.TypeOf((*safeEnumer)(nil)).Elem()) {
continue
}

fieldValue := tvalue.FieldByName(fieldType.Name)

inner := fieldValue.Interface().(safeEnumer).newsafeenum(s)
fieldValue.Set(reflect.ValueOf(inner))

return core.MapAny(core.GetAvailableEnumValue[T](), t, s)
}

panic("something wrong")
}

func (e SafeEnum[underlyingEnum]) IsValid() bool {
Expand Down Expand Up @@ -66,3 +98,13 @@ func (e SafeEnum[underlyingEnum]) GoString() string {

return fmt.Sprintf("%d (%s)", ToInt(e), e.inner)
}

// WARNING: Only use this function if you fully understand its behavior.
// It might cause unexpected results if used improperly.
func (e SafeEnum[underlyingEnum]) newsafeenum(s string) any {
return core.MapAny(
core.GetAvailableEnumValue[SafeEnum[underlyingEnum]](),
SafeEnum[underlyingEnum]{inner: s},
s,
)
}
Loading

0 comments on commit 5bff66a

Please sign in to comment.