Skip to content

Commit

Permalink
add nullable (#39)
Browse files Browse the repository at this point in the history
  • Loading branch information
huykingsofm authored Dec 18, 2024
1 parent e367a3c commit b1015ca
Show file tree
Hide file tree
Showing 12 changed files with 595 additions and 50 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ jobs:
- name: Test
run: |
go test -timeout 30s -v -race ./...
go test -timeout 30s -v -race ./testing/...
go test -timeout 30s -v -race ./testing/... -coverpkg=.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ go.work.sum

# env file
.env

# Vscode
.vscode
28 changes: 18 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,24 @@ Currently supported:
- `JSON`: Implements `json.Marshaler` and `json.Unmarshaler`.
- `SQL`: Implements `driver.Valuer` and `sql.Scanner`.

## 🔅 Nullable

The `Nullable` option allows handling nullable enums in both JSON and SQL.

```go
type Role int
type NullRole = enum.Nullable[Role]

type User struct {
ID int `json:"id"`
Role NullRole `json:"role"`
}

func main() {
fmt.Println(json.Marshal(User{})) // {"id": 0, "user": null}
}
```

## 🔅 Type safety

The [WrapEnum][2] prevents most invalid enum cases due to built-in methods for serialization and deserialization, offering **basic type safety**.
Expand Down Expand Up @@ -340,13 +358,3 @@ The benchmark results are based on defining an enum with 10 values at [bench](./
| SQL Value | 38 ns | 29 ns |
| SQL Scan bytes | 41 ns | 29 ns |
| SQL Scan string | 22 ns | 15 ns |


## V2 roadmap

- Switch from `json.Marshaler` to `encoding.TextMarshaler` for better performance.
- New function allocates from 1 instead 0.
- Allow defining which enum supports default value or nil value.
- Remove deprecated items.

**Contributions and suggestions to refine the roadmap are welcome!**
5 changes: 3 additions & 2 deletions enum.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,8 @@ func ToString[Enum any](value Enum) string {
// ToInt returns the int representation for the given enum value. It returns the
// smallest value of int (math.MinInt32) for invalid enums.
//
// DEPRECATED: It is only valid if the enum is not a floating-point number.
// DEPRECATED: This function returns math.MinInt32 for invalid enums, which may
// cause unexpected behavior.
func ToInt[Enum any](enum Enum) int {
value, ok := mtmap.Get2(mtkey.Enum2Number[Enum, int](enum))
if !ok {
Expand Down Expand Up @@ -306,7 +307,7 @@ func All[Enum any]() []Enum {
return mtmap.Get(mtkey.AllEnums[Enum]())
}

var advancedEnumNames = []string{"WrapEnum", "SafeEnum"}
var advancedEnumNames = []string{"WrapEnum", "WrapUintEnum", "WrapFloatEnum", "SafeEnum"}

// NameOf returns the name of the enum type. In case of this is an advanced enum
// provided by this library, this function returns the only underlying enum
Expand Down
36 changes: 32 additions & 4 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ func ExampleMap() {
}

func ExampleWrapEnum() {
// Define a generic enum type
type role any
type Role = enum.WrapEnum[role]

Expand Down Expand Up @@ -104,18 +103,16 @@ func ExampleWrapEnum() {
}

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

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

// As Role is a StructEnum, it can utilize methods from StructEnum, including
// As Role is a SafeEnum, it can utilize methods from SafeEnum, including
// utility functions and serde operations.
fmt.Println(RoleUser.GoString()) // 0 (user)
fmt.Println(RoleUser.IsValid()) // true
Expand Down Expand Up @@ -150,3 +147,34 @@ func ExampleSafeEnum() {
// {"id":0,"name":"tester","role":"admin"}
// admin
}

func ExampleNullable() {
type Role int
type NullRole = enum.Nullable[Role]

var (
_ = enum.New[Role]("user")
RoleAdmin = enum.New[Role]("admin")
_ = enum.Finalize[Role]() // Optional: ensure no new enum values can be added to Role.
)

// Define a struct that includes the Role enum.
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Role NullRole `json:"role"`
}

// Serialize zero struct
data, _ := json.Marshal(User{})
fmt.Println(string(data))

// Serialize the User struct to JSON.
user1 := User{ID: 0, Name: "tester", Role: NullRole{Enum: RoleAdmin, Valid: true}}
data, _ = json.Marshal(user1)
fmt.Println(string(data))

// Output:
// {"id":0,"name":"","role":null}
// {"id":0,"name":"tester","role":"admin"}
}
6 changes: 3 additions & 3 deletions internal/core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import (
"github.com/xybor-x/enum/internal/xreflect"
)

func GetAvailableEnumValue[T any]() int64 {
func GetAvailableEnumValue[Enum any]() int64 {
id := int64(0)
for {
if _, ok := mtmap.Get2(mtkey.Number2Enum[int64, T](id)); !ok {
if _, ok := mtmap.Get2(mtkey.Number2Enum[int64, Enum](id)); !ok {
break
}
id++
Expand All @@ -24,7 +24,7 @@ func GetAvailableEnumValue[T any]() int64 {

// MapAny map the enum value to the enum system.
func MapAny[N xreflect.Number, Enum any](id N, enum Enum, s string) Enum {
if ok := mtmap.Get(mtkey.IsFinalized[Enum]()); ok {
if mtmap.Get(mtkey.IsFinalized[Enum]()) {
panic("enum is finalized")
}

Expand Down
46 changes: 46 additions & 0 deletions nullable.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package enum

import "database/sql/driver"

// Nullable allows handling nullable enums in both JSON and SQL.
type Nullable[Enum any] struct {
Enum Enum
Valid bool
}

func (e Nullable[Enum]) MarshalJSON() ([]byte, error) {
if !e.Valid {
return []byte("null"), nil
}

return MarshalJSON(e.Enum)
}

func (e *Nullable[Enum]) UnmarshalJSON(data []byte) error {
if string(data) == "null" {
var defaultEnum Enum
e.Enum, e.Valid = defaultEnum, false
return nil
}

return UnmarshalJSON(data, &e.Enum)
}

func (e Nullable[Enum]) Value() (driver.Value, error) {
if !e.Valid {
return nil, nil
}

return ValueSQL(e.Enum)
}

func (e *Nullable[Enum]) Scan(a any) error {
if a == nil {
var defaultEnum Enum
e.Enum, e.Valid = defaultEnum, false
return nil
}

e.Valid = true
return ScanSQL(a, &e.Enum)
}
2 changes: 1 addition & 1 deletion safe_enum.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func (e SafeEnum[underlyingEnum]) GoString() string {
return "<nil>"
}

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

// WARNING: Only use this function if you fully understand its behavior.
Expand Down
67 changes: 39 additions & 28 deletions testing/enum_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package testing
package testing_test

import (
"database/sql"
"encoding/json"
"fmt"
"math"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -78,8 +77,8 @@ func TestEnumMapDiffEnumber(t *testing.T) {
_ = enum.Map(RoleAdmin, "admin")
)

assert.Equal(t, enum.ToInt(RoleUser), 1)
assert.Equal(t, enum.ToInt(RoleAdmin), 2)
assert.Equal(t, int(RoleUser), 1)
assert.Equal(t, int(RoleAdmin), 2)
}

func TestEnumFinalize(t *testing.T) {
Expand Down Expand Up @@ -160,54 +159,39 @@ func TestEnumMustFromString(t *testing.T) {
assert.Panics(t, func() { enum.MustFromString[Role]("moderator") })
}

func TestEnumFromInt(t *testing.T) {
func TestEnumFromNumber(t *testing.T) {
type Role int

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

userRole, ok := enum.FromInt[Role](0)
userRole, ok := enum.FromNumber[Role](0)
assert.True(t, ok)
assert.Equal(t, userRole, RoleUser)

adminRole, ok := enum.FromInt[Role](1)
adminRole, ok := enum.FromNumber[Role](1)
assert.True(t, ok)
assert.Equal(t, adminRole, RoleAdmin)

_, ok = enum.FromString[Role]("moderator")
assert.False(t, ok)
}

func TestEnumMustFromInt(t *testing.T) {
func TestEnumMustFromNumber(t *testing.T) {
type Role int

assert.Panics(t, func() { enum.MustFromInt[Role](0) })
assert.Panics(t, func() { enum.MustFromNumber[Role](0) })

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

assert.Equal(t, enum.MustFromInt[Role](0), RoleUser)
assert.Equal(t, enum.MustFromInt[Role](1), RoleAdmin)
assert.Panics(t, func() { enum.MustFromInt[Role](2) })
}

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

assert.Equal(t, enum.ToInt(Role(42)), math.MinInt32)

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

assert.Equal(t, enum.ToInt(RoleUser), 0)
assert.Equal(t, enum.ToInt(RoleAdmin), 1)
assert.Equal(t, enum.ToInt(Role(42)), math.MinInt32)
assert.Equal(t, enum.MustFromNumber[Role](0), RoleUser)
assert.Equal(t, enum.MustFromNumber[Role](1), RoleAdmin)
assert.Panics(t, func() { enum.MustFromNumber[Role](2) })
}

func TestEnumUndefined(t *testing.T) {
Expand Down Expand Up @@ -311,6 +295,19 @@ func TestEnumFloat32(t *testing.T) {
assert.Equal(t, []Role{RoleUser, RoleAdmin}, enum.All[Role]())
}

func TestEnumFloat64(t *testing.T) {
type Role float64

assert.Nil(t, enum.All[Role]())

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

assert.Equal(t, []Role{RoleUser, RoleAdmin}, enum.All[Role]())
}

func TestEnumFloat32Map(t *testing.T) {
type Role float32

Expand Down Expand Up @@ -418,6 +415,20 @@ func TestEnumIntFromFloat64(t *testing.T) {
assert.Equal(t, RoleAdmin, role)
}

func TestEnumNameOf(t *testing.T) {
type Role int
assert.Equal(t, "Role", enum.NameOf[Role]())
assert.Equal(t, "Role", enum.NameOf[Role]())

type weekday any
type Weekday = enum.WrapEnum[weekday]
assert.Equal(t, "Weekday", enum.NameOf[Weekday]())

type someURL any
type SomeURL = enum.WrapEnum[someURL]
assert.Equal(t, "SomeURL", enum.NameOf[SomeURL]())
}

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

Expand Down Expand Up @@ -532,7 +543,7 @@ func TestEnumPrintZeroStruct(t *testing.T) {
assert.Equal(t, "{0}", fmt.Sprint(User{}))
}

func TestNewExtendedSafe(t *testing.T) {
func TestNewExtended(t *testing.T) {
type Role struct{ enum.SafeEnum[int] }

var (
Expand Down
Loading

0 comments on commit b1015ca

Please sign in to comment.