Skip to content

Commit

Permalink
support yaml
Browse files Browse the repository at this point in the history
  • Loading branch information
huykingsofm committed Jan 2, 2025
1 parent 9a0a3aa commit d9f3246
Show file tree
Hide file tree
Showing 10 changed files with 262 additions and 12 deletions.
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
38 changes: 38 additions & 0 deletions enum.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,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 @@ -307,13 +308,50 @@ 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("invalid yaml kind: only supports scalar")
}

// 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)
}

Expand Down
29 changes: 27 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,27 @@ 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 {
if node.Kind != yaml.ScalarNode && node.Tag == "!!null" {
var defaultEnum Enum
e.Enum, e.Valid = defaultEnum, false
return nil
}

return UnmarshalYAML(node, &e.Enum)
}

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

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

var _ newableEnum = SafeEnum[int]{}
Expand Down Expand Up @@ -42,6 +43,14 @@ func (e *SafeEnum[underlyingEnum]) UnmarshalXML(decoder *xml.Decoder, start xml.
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
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
)
70 changes: 70 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,72 @@ 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)
}

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)
}
79 changes: 79 additions & 0 deletions testing/wrap_enum_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package testing_test

import (
"encoding/json"
"encoding/xml"
"testing"

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

func TestWrapEnumMarshalJSON(t *testing.T) {
Expand Down Expand Up @@ -307,3 +309,80 @@ func TestSafeEnumScanSQL(t *testing.T) {
err = data.Scan("admin")
assert.ErrorContains(t, err, "enum SafeEnum[role]: unknown string admin")
}

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

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

type Test1 struct {
Role Role `xml:"CustomRole"`
}

data, err := xml.Marshal(Test1{Role: RoleUser})
assert.NoError(t, err)
assert.Equal(t, "<Test1><CustomRole>user</CustomRole></Test1>", string(data))

type Test2 struct {
Role Role
}

data, err = xml.Marshal(Test2{Role: RoleUser})
assert.NoError(t, err)
assert.Equal(t, "<Test2><Role>user</Role></Test2>", string(data))
}

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

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

var data Role

err := xml.Unmarshal([]byte(`<Role>user</Role>`), &data)
assert.NoError(t, err)
assert.Equal(t, RoleUser, data)

err = xml.Unmarshal([]byte(`<Role>admin</Role>`), &data)
assert.ErrorContains(t, err, "enum WrapEnum[role]: unknown string admin")
}

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

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

data, err := yaml.Marshal(RoleUser)
assert.NoError(t, err)
assert.Equal(t, "user\n", string(data))

_, err = yaml.Marshal(Role(1))
assert.ErrorContains(t, err, "enum WrapEnum[role]: invalid value 1")
}

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

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

var data Role

err := yaml.Unmarshal([]byte(`user`), &data)
assert.NoError(t, err)
assert.Equal(t, RoleUser, data)

err = yaml.Unmarshal([]byte(`admin`), &data)
assert.ErrorContains(t, err, "enum WrapEnum[role]: unknown string admin")
}
9 changes: 9 additions & 0 deletions wrap_float_enum.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

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

var _ newableEnum = WrapFloatEnum[int](0)
Expand Down Expand Up @@ -36,6 +37,14 @@ func (e *WrapFloatEnum[underlyingEnum]) UnmarshalXML(decoder *xml.Decoder, start
return UnmarshalXML(decoder, start, e)
}

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

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

func (e WrapFloatEnum[underlyingEnum]) Value() (driver.Value, error) {
return ValueSQL(e)
}
Expand Down
9 changes: 9 additions & 0 deletions wrap_int_enum.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

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

var _ newableEnum = WrapEnum[int](0)
Expand Down Expand Up @@ -36,6 +37,14 @@ func (e *WrapEnum[underlyingEnum]) UnmarshalXML(decoder *xml.Decoder, start xml.
return UnmarshalXML(decoder, start, e)
}

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

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

func (e WrapEnum[underlyingEnum]) Value() (driver.Value, error) {
return ValueSQL(e)
}
Expand Down
Loading

0 comments on commit d9f3246

Please sign in to comment.