Skip to content

Commit

Permalink
add nullable
Browse files Browse the repository at this point in the history
  • Loading branch information
huykingsofm committed Dec 18, 2024
1 parent e367a3c commit 27c5248
Show file tree
Hide file tree
Showing 14 changed files with 642 additions and 96 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
30 changes: 19 additions & 11 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 @@ -335,18 +353,8 @@ The benchmark results are based on defining an enum with 10 values at [bench](./
| --------------- | -------------: | --------------: |
| ToString | 17 ns | 6 ns |
| FromString | 22 ns | 15 ns |
| json.Marshal | 148 ns | 113 ns |
| json.Marshal | 118 ns | 113 ns |
| json.Unmarshal | 144 ns | 147 ns |
| 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!**
60 changes: 29 additions & 31 deletions enum.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ package enum
import (
"database/sql/driver"
"fmt"
"math"
"path"
"reflect"
"strings"
Expand Down Expand Up @@ -152,34 +151,12 @@ func Finalize[Enum any]() bool {
return true
}

// FromInt returns the corresponding enum for a given int representation, and
// whether it is valid.
//
// DEPRECATED: Use FromNumber instead.
func FromInt[Enum any](i int) (Enum, bool) {
return mtmap.Get2(mtkey.Number2Enum[int, Enum](i))
}

// FromNumber returns the corresponding enum for a given number representation,
// and whether it is valid.
func FromNumber[Enum any, N xreflect.Number](n N) (Enum, bool) {
return mtmap.Get2(mtkey.Number2Enum[N, Enum](n))
}

// MustFromInt returns the corresponding enum for a given int representation.
//
// It panics if the enum value is invalid.
//
// DEPRECATED: Use MustFromNumber instead.
func MustFromInt[Enum any](i int) Enum {
t, ok := FromInt[Enum](i)
if !ok {
panic(fmt.Sprintf("enum %s: invalid int %d", TrueNameOf[Enum](), i))
}

return t
}

// MustFromNumber returns the corresponding enum for a given number
// representation.
//
Expand Down Expand Up @@ -223,17 +200,16 @@ func ToString[Enum any](value Enum) string {
return str
}

// ToInt returns the int representation for the given enum value. It returns the
// smallest value of int (math.MinInt32) for invalid enums.
// MustToNumber returns the numeric representation for the given enum value.
//
// DEPRECATED: It is only valid if the enum is not a floating-point number.
func ToInt[Enum any](enum Enum) int {
value, ok := mtmap.Get2(mtkey.Enum2Number[Enum, int](enum))
// It panics if the provided enum is invalid. Use it with caution.
func MustToNumber[N xreflect.Number, Enum any](enum Enum) N {
n, ok := mtmap.Get2(mtkey.Enum2Number[Enum, N](enum))
if !ok {
return math.MinInt32
panic(fmt.Sprintf("enum %s: invalid enum %#v", TrueNameOf[Enum](), enum))
}

return value
return n
}

// IsValid checks if an enum value is valid.
Expand Down Expand Up @@ -270,6 +246,28 @@ func UnmarshalJSON[Enum any](data []byte, t *Enum) (err error) {
return nil
}

// MarshalText serializes an enum value into its string representation.
func MarshalText[Enum any](value Enum) ([]byte, error) {
s, ok := mtmap.Get2(mtkey.Enum2String(value))
if !ok {
return nil, fmt.Errorf("enum %s: invalid value %#v", TrueNameOf[Enum](), value)
}

return []byte(s), nil
}

// UnmarshalText deserializes a string representation of an enum value from
// JSON.
func UnmarshalText[Enum any](data []byte, t *Enum) (err error) {
enum, ok := mtmap.Get2(mtkey.String2Enum[Enum](string(data)))
if !ok {
return fmt.Errorf("enum %s: unknown string %s", TrueNameOf[Enum](), string(data))
}

*t = enum
return nil
}

// ValueSQL serializes an enum into a database-compatible format.
func ValueSQL[Enum any](value Enum) (driver.Value, error) {
str, ok := mtmap.Get2(mtkey.Enum2String(value))
Expand Down Expand Up @@ -306,7 +304,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
48 changes: 48 additions & 0 deletions nullable.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
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)
}
10 changes: 5 additions & 5 deletions safe_enum.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ func (e SafeEnum[underlyingEnum]) IsValid() bool {
return IsValid(e)
}

func (e SafeEnum[underlyingEnum]) MarshalJSON() ([]byte, error) {
return MarshalJSON(e)
func (e SafeEnum[underlyingEnum]) MarshalText() ([]byte, error) {
return MarshalText(e)
}

func (e *SafeEnum[underlyingEnum]) UnmarshalJSON(data []byte) error {
return UnmarshalJSON(data, e)
func (e *SafeEnum[underlyingEnum]) UnmarshalText(data []byte) error {
return UnmarshalText(data, e)
}

func (e SafeEnum[underlyingEnum]) Value() (driver.Value, error) {
Expand All @@ -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
Loading

0 comments on commit 27c5248

Please sign in to comment.