Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge dev #48

Merged
merged 2 commits into from
Jan 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,10 +231,12 @@ Serialization and deserialization are essential when working with enums, and our
Currently supported:
- `JSON`: Implements `json.Marshaler` and `json.Unmarshaler`.
- `SQL`: Implements `driver.Valuer` and `sql.Scanner`.
- `YAML`: Implements `yaml.Marshaler` and `yaml.Unmarshaler`.
- `XML`: Implements `xml.Marshaler` and `xml.Unmarshaler`.

## 🔅 Nullable

The `Nullable` transforms an enum type into a nullable enum, akin to `sql.NullXXX`, and is designed to handle nullable values in both JSON and SQL.
The `Nullable` transforms an enum type into a nullable enum, akin to `sql.NullXXX`, and is designed to handle nullable values in both JSON, YAML, and SQL.

```go
type Role int
Expand Down
71 changes: 68 additions & 3 deletions enum.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ package enum

import (
"database/sql/driver"
"encoding/xml"
"fmt"
"math"
"reflect"
Expand All @@ -20,6 +21,7 @@ import (
"github.com/xybor-x/enum/internal/mtkey"
"github.com/xybor-x/enum/internal/mtmap"
"github.com/xybor-x/enum/internal/xreflect"
"gopkg.in/yaml.v3"
)

// newableEnum is an internal interface used for handling centralized
Expand Down Expand Up @@ -266,14 +268,14 @@ func To[P, Enum any](enum Enum) (P, bool) {

// MustTo returns the representation (the type is relied on P type parameter)
// for the given enum value. It returns zero value if the enum is invalid or the
// enum doesn't have any representation of type P..
// enum doesn't have any representation of type P.
func MustTo[P, Enum any](enum Enum) P {
val, _ := To[P](enum)
return val
}

// IsValid checks if an enum value is valid.
// It returns true if the enum value is valid, and false otherwise.
// IsValid checks if an enum value is valid. It returns true if the enum value
// is valid, and false otherwise.
func IsValid[Enum any](value Enum) bool {
_, ok := mtmap.Get2(mtkey.Enum2Repr[Enum, string](value))
return ok
Expand Down Expand Up @@ -306,6 +308,69 @@ func UnmarshalJSON[Enum any](data []byte, t *Enum) (err error) {
return nil
}

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

return s, nil
}

// UnmarshalYAML deserializes a string representation of an enum value from
// YAML.
func UnmarshalYAML[Enum any](value *yaml.Node, t *Enum) error {
// Check if the value is a scalar (string in this case)
if value.Kind != yaml.ScalarNode {
return fmt.Errorf("enum %s: only supports scalar in yaml enum", TrueNameOf[Enum]())
}

// Assign the string value directly
var s string
if err := value.Decode(&s); err != nil {
return err
}

var ok bool
*t, ok = From[Enum](s)
if !ok {
return fmt.Errorf("enum %s: unknown string %s", TrueNameOf[Enum](), s)
}

return nil
}

// MarshalXML converts enum to its string representation.
func MarshalXML[Enum any](encoder *xml.Encoder, start xml.StartElement, enum Enum) error {
str, ok := To[string](enum)
if !ok {
return fmt.Errorf("enum %s: invalid value %#v", TrueNameOf[Enum](), enum)
}

if start.Name.Local == "" {
start.Name.Local = NameOf[Enum]()
}

return encoder.EncodeElement(str, start)
}

// UnmarshalXML parses the string representation back into an enum.
func UnmarshalXML[Enum any](decoder *xml.Decoder, start xml.StartElement, enum *Enum) error {
var str string
if err := decoder.DecodeElement(&str, &start); err != nil {
return err
}

val, ok := FromString[Enum](str)
if !ok {
return fmt.Errorf("enum %s: unknown string %s", TrueNameOf[Enum](), str)
}

*enum = val
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.Enum2Repr[Enum, string](value))
Expand Down
26 changes: 24 additions & 2 deletions nullable.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package enum

import "database/sql/driver"
import (
"database/sql/driver"

// Nullable allows handling nullable enums in both JSON and SQL.
"gopkg.in/yaml.v3"
)

// Nullable allows handling nullable enums in JSON, YAML, and SQL.
type Nullable[Enum any] struct {
Enum Enum
Valid bool
Expand All @@ -26,6 +30,24 @@ func (e *Nullable[Enum]) UnmarshalJSON(data []byte) error {
return UnmarshalJSON(data, &e.Enum)
}

func (e Nullable[Enum]) MarshalYAML() (any, error) {
if !e.Valid {
return yaml.Node{
Kind: yaml.ScalarNode,
Tag: "!!null", // Use the YAML null tag
}, nil
}

return MarshalYAML(e.Enum)
}

func (e *Nullable[Enum]) UnmarshalYAML(node *yaml.Node) error {
// NOTE: Currently, yaml.Unmarshal will not trigger UnmarshalYAML in case of
// null. That's the reason why we only need to handle the non-null value
// here.
return UnmarshalYAML(node, &e.Enum)
}

func (e Nullable[Enum]) Value() (driver.Value, error) {
if !e.Valid {
return nil, nil
Expand Down
18 changes: 18 additions & 0 deletions safe_enum.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package enum

import (
"database/sql/driver"
"encoding/xml"
"fmt"

"github.com/xybor-x/enum/internal/core"
"gopkg.in/yaml.v3"
)

var _ newableEnum = SafeEnum[int]{}
Expand Down Expand Up @@ -33,6 +35,22 @@ func (e *SafeEnum[underlyingEnum]) UnmarshalJSON(data []byte) error {
return UnmarshalJSON(data, e)
}

func (e SafeEnum[underlyingEnum]) MarshalXML(encoder *xml.Encoder, start xml.StartElement) error {
return MarshalXML(encoder, start, e)
}

func (e *SafeEnum[underlyingEnum]) UnmarshalXML(decoder *xml.Decoder, start xml.StartElement) error {
return UnmarshalXML(decoder, start, e)
}

func (e SafeEnum[underlyingEnum]) MarshalYAML() (any, error) {
return MarshalYAML(e)
}

func (e *SafeEnum[underlyingEnum]) UnmarshalYAML(node *yaml.Node) error {
return UnmarshalYAML(node, e)
}

func (e SafeEnum[underlyingEnum]) Value() (driver.Value, error) {
return ValueSQL(e)
}
Expand Down
40 changes: 36 additions & 4 deletions testing/enum_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package testing_test
import (
"database/sql"
"encoding/json"
"encoding/xml"
"fmt"
"testing"

Expand Down Expand Up @@ -501,7 +502,8 @@ func TestEnumJSON(t *testing.T) {
type Role = enum.WrapEnum[role]

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

type TestJSON struct {
Expand All @@ -518,11 +520,41 @@ func TestEnumJSON(t *testing.T) {

data, err := json.Marshal(s)
assert.NoError(t, err)
assert.Equal(t, "{\"id\":1,\"name\":\"tester\",\"role\":\"user\"}", string(data))
assert.Equal(t, `{"id":1,"name":"tester","role":"user"}`, string(data))

err = json.Unmarshal([]byte(`{"id":1,"name":"tester","role":"admin"}`), &s)
assert.NoError(t, err)
assert.Equal(t, RoleAdmin, s.Role)
}

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

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

type TestXML struct {
ID int `xml:"id"`
Name string `xml:"name"`
Role Role `xml:"role"`
}

s := TestXML{
ID: 1,
Name: "tester",
Role: RoleUser,
}

data, err := xml.Marshal(s)
assert.NoError(t, err)
assert.Equal(t, "<TestXML><id>1</id><name>tester</name><role>user</role></TestXML>", string(data))

err = json.Unmarshal([]byte("{\"id\":1,\"name\":\"tester\",\"role\":\"user\"}"), &s)
err = xml.Unmarshal([]byte("<TestXML><id>1</id><name>tester</name><role>admin</role></TestXML>"), &s)
assert.NoError(t, err)
assert.Equal(t, RoleUser, s.Role)
assert.Equal(t, RoleAdmin, s.Role)
}

func TestEnumPrintZeroStruct(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion testing/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ require (
github.com/stretchr/testify v1.10.0
github.com/xybor-x/enum v0.3.0
google.golang.org/protobuf v1.36.0
gopkg.in/yaml.v3 v3.0.1
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
73 changes: 73 additions & 0 deletions testing/nullable_enum_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/stretchr/testify/assert"
"github.com/xybor-x/enum"
"gopkg.in/yaml.v3"
)

func TestNullableJSON(t *testing.T) {
Expand Down Expand Up @@ -136,3 +137,75 @@ func TestNullableSQLNull(t *testing.T) {
// Check if the deserialized value matches the expected value
assert.False(t, retrievedRole.Valid)
}

func TestNullableYAML(t *testing.T) {
type role any
type Role = enum.WrapEnum[role]
type NullRole = enum.Nullable[Role]

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

type TestYAML struct {
ID int `yaml:"id"`
Name string `yaml:"name"`
Role NullRole `yaml:"role"`
}

s := TestYAML{
ID: 1,
Name: "tester",
Role: NullRole{Enum: RoleUser, Valid: true},
}

data, err := yaml.Marshal(s)
assert.NoError(t, err)
assert.Equal(t, "id: 1\nname: tester\nrole: user\n", string(data))

err = yaml.Unmarshal([]byte("id: 1\nname: tester\nrole: user\n"), &s)
assert.NoError(t, err)
assert.True(t, s.Role.Valid)
assert.Equal(t, RoleUser, s.Role.Enum)

err = yaml.Unmarshal([]byte("id: 1\nname: tester\nrole:\n- user\n"), &s)
assert.ErrorContains(t, err, "enum WrapEnum[role]: only supports scalar in yaml enum")
}

func TestNullableYAMLNull(t *testing.T) {
type role any
type Role = enum.WrapEnum[role]
type NullRole = enum.Nullable[Role]

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

type TestYAML struct {
ID int `yaml:"id"`
Name string `yaml:"name"`
Role NullRole `yaml:"role"`
}

s := TestYAML{
ID: 1,
Name: "tester",
Role: NullRole{},
}

data, err := yaml.Marshal(s)
assert.NoError(t, err)
assert.Equal(t, "id: 1\nname: tester\nrole:\n", string(data))

err = yaml.Unmarshal([]byte("id: 1\nname: tester\nrole:\n"), &s)
assert.NoError(t, err)
assert.False(t, s.Role.Valid)

err = yaml.Unmarshal([]byte("id: 1\nname: tester\nrole: null\n"), &s)
assert.NoError(t, err)
assert.False(t, s.Role.Valid)

err = yaml.Unmarshal([]byte("id: 1\nname: tester\nrole: ~\n"), &s)
assert.NoError(t, err)
assert.False(t, s.Role.Valid)
}
Loading
Loading