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