diff --git a/docs.md b/docs.md
index b6e9ee7..d334178 100644
--- a/docs.md
+++ b/docs.md
@@ -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
diff --git a/enum.go b/enum.go
index e7b9f99..ea338e7 100644
--- a/enum.go
+++ b/enum.go
@@ -12,6 +12,7 @@ package enum
import (
"database/sql/driver"
+ "encoding/xml"
"fmt"
"math"
"reflect"
@@ -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
@@ -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
@@ -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))
diff --git a/nullable.go b/nullable.go
index cb47954..4df9312 100644
--- a/nullable.go
+++ b/nullable.go
@@ -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
@@ -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
diff --git a/safe_enum.go b/safe_enum.go
index 4d4c3ab..564b4d1 100644
--- a/safe_enum.go
+++ b/safe_enum.go
@@ -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]{}
@@ -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)
}
diff --git a/testing/enum_test.go b/testing/enum_test.go
index b2dff90..4e1601f 100644
--- a/testing/enum_test.go
+++ b/testing/enum_test.go
@@ -3,6 +3,7 @@ package testing_test
import (
"database/sql"
"encoding/json"
+ "encoding/xml"
"fmt"
"testing"
@@ -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 {
@@ -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, "1testeruser", string(data))
- err = json.Unmarshal([]byte("{\"id\":1,\"name\":\"tester\",\"role\":\"user\"}"), &s)
+ err = xml.Unmarshal([]byte("1testeradmin"), &s)
assert.NoError(t, err)
- assert.Equal(t, RoleUser, s.Role)
+ assert.Equal(t, RoleAdmin, s.Role)
}
func TestEnumPrintZeroStruct(t *testing.T) {
diff --git a/testing/go.mod b/testing/go.mod
index 7840647..5b7035b 100644
--- a/testing/go.mod
+++ b/testing/go.mod
@@ -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
)
diff --git a/testing/nullable_enum_test.go b/testing/nullable_enum_test.go
index 0e70382..6f6594b 100644
--- a/testing/nullable_enum_test.go
+++ b/testing/nullable_enum_test.go
@@ -7,6 +7,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/xybor-x/enum"
+ "gopkg.in/yaml.v3"
)
func TestNullableJSON(t *testing.T) {
@@ -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)
+}
diff --git a/testing/wrap_enum_test.go b/testing/wrap_enum_test.go
index e7ec655..d8bab4f 100644
--- a/testing/wrap_enum_test.go
+++ b/testing/wrap_enum_test.go
@@ -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) {
@@ -307,3 +309,86 @@ 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, "user", string(data))
+
+ type Test2 struct {
+ Role Role
+ }
+
+ data, err = xml.Marshal(Test2{Role: RoleUser})
+ assert.NoError(t, err)
+ assert.Equal(t, "user", 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(`user`), &data)
+ assert.NoError(t, err)
+ assert.Equal(t, RoleUser, data)
+
+ err = xml.Unmarshal([]byte(`admin`), &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")
+ )
+
+ type Test struct {
+ Role Role `yaml:"role"`
+ }
+ var data Test
+
+ err := yaml.Unmarshal([]byte(`role: user`), &data)
+ assert.NoError(t, err)
+ assert.Equal(t, RoleUser, data.Role)
+
+ err = yaml.Unmarshal([]byte("role: admin"), &data)
+ assert.ErrorContains(t, err, "enum WrapEnum[role]: unknown string admin")
+
+ err = yaml.Unmarshal([]byte("role:\n- user\n"), &data)
+ assert.ErrorContains(t, err, "enum WrapEnum[role]: only supports scalar in yaml enum")
+}
diff --git a/wrap_float_enum.go b/wrap_float_enum.go
index c5b6338..a0606cb 100644
--- a/wrap_float_enum.go
+++ b/wrap_float_enum.go
@@ -2,10 +2,12 @@ package enum
import (
"database/sql/driver"
+ "encoding/xml"
"fmt"
"github.com/xybor-x/enum/internal/core"
"github.com/xybor-x/enum/internal/xreflect"
+ "gopkg.in/yaml.v3"
)
var _ newableEnum = WrapFloatEnum[int](0)
@@ -27,6 +29,22 @@ func (e *WrapFloatEnum[underlyingEnum]) UnmarshalJSON(data []byte) error {
return UnmarshalJSON(data, e)
}
+func (e WrapFloatEnum[underlyingEnum]) MarshalXML(encoder *xml.Encoder, start xml.StartElement) error {
+ return MarshalXML(encoder, start, e)
+}
+
+func (e *WrapFloatEnum[underlyingEnum]) UnmarshalXML(decoder *xml.Decoder, start xml.StartElement) error {
+ 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)
}
diff --git a/wrap_int_enum.go b/wrap_int_enum.go
index dfad782..697a065 100644
--- a/wrap_int_enum.go
+++ b/wrap_int_enum.go
@@ -2,10 +2,12 @@ package enum
import (
"database/sql/driver"
+ "encoding/xml"
"fmt"
"github.com/xybor-x/enum/internal/core"
"github.com/xybor-x/enum/internal/xreflect"
+ "gopkg.in/yaml.v3"
)
var _ newableEnum = WrapEnum[int](0)
@@ -27,6 +29,22 @@ func (e *WrapEnum[underlyingEnum]) UnmarshalJSON(data []byte) error {
return UnmarshalJSON(data, e)
}
+func (e WrapEnum[underlyingEnum]) MarshalXML(encoder *xml.Encoder, start xml.StartElement) error {
+ return MarshalXML(encoder, start, e)
+}
+
+func (e *WrapEnum[underlyingEnum]) UnmarshalXML(decoder *xml.Decoder, start xml.StartElement) error {
+ 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)
}
diff --git a/wrap_uint_enum.go b/wrap_uint_enum.go
index 0ccea0d..acfda6d 100644
--- a/wrap_uint_enum.go
+++ b/wrap_uint_enum.go
@@ -2,10 +2,12 @@ package enum
import (
"database/sql/driver"
+ "encoding/xml"
"fmt"
"github.com/xybor-x/enum/internal/core"
"github.com/xybor-x/enum/internal/xreflect"
+ "gopkg.in/yaml.v3"
)
var _ newableEnum = WrapUintEnum[int](0)
@@ -27,6 +29,22 @@ func (e *WrapUintEnum[underlyingEnum]) UnmarshalJSON(data []byte) error {
return UnmarshalJSON(data, e)
}
+func (e WrapUintEnum[underlyingEnum]) MarshalXML(encoder *xml.Encoder, start xml.StartElement) error {
+ return MarshalXML(encoder, start, e)
+}
+
+func (e *WrapUintEnum[underlyingEnum]) UnmarshalXML(decoder *xml.Decoder, start xml.StartElement) error {
+ return UnmarshalXML(decoder, start, e)
+}
+
+func (e WrapUintEnum[underlyingEnum]) MarshalYAML() (any, error) {
+ return MarshalYAML(e)
+}
+
+func (e *WrapUintEnum[underlyingEnum]) UnmarshalYAML(node *yaml.Node) error {
+ return UnmarshalYAML(node, e)
+}
+
func (e WrapUintEnum[underlyingEnum]) Value() (driver.Value, error) {
return ValueSQL(e)
}