diff --git a/CHANGELOG.md b/CHANGELOG.md index 37472ac..373c4b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,14 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Added +- The Any type added as an alias on interface{} in the library style. +- The encode methods created for Any to work in MessagePack. +- Tests added for Any. + ### Changed +- Corrections made like interface{} -> any as required by the linter. + ### Fixed ## [v1.0.0] - 2025-09-09 diff --git a/any_gen.go b/any_gen.go new file mode 100644 index 0000000..2d268ff --- /dev/null +++ b/any_gen.go @@ -0,0 +1,183 @@ +package option + +import ( + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" +) + +// Any represents an optional value of type any. +// It can either hold a valid Any (any type like string or int, float). +// (IsSome == true) or be empty (IsZero == true). +type Any struct { //nolint:recvcheck + value any + exists bool +} + +var _ commonInterface[any] = (*Any)(nil) + +// SomeAny creates an optional Any with the given value. +// The returned Any (any type) will have IsSome() == true and IsZero() == false. +// +// Example: +// +// o := SomeAny(7777.7777777) +// if o.IsSome() { +// v := o.Unwrap() // v == true +// } +func SomeAny(value any) Any { + return Any{ + value: value, + exists: true, + } +} + +// NoneAny creates an empty optional any value. +// The returned Any will have IsSome() == false and IsZero() == true. +// +// Example: +// +// o := NoneAny() +// if o.IsZero() { +// fmt.Println("value is absent") +// } +func NoneAny() Any { + return Any{ + exists: false, + value: zero[any](), + } +} + +// IsSome returns true if the Any contains a value. +// This indicates the value is explicitly set (not None). +func (o Any) IsSome() bool { + return o.exists +} + +// IsZero returns true if the Any does not contain a value. +// Equivalent to !IsSome(). Useful for consistency with types where +// zero value (e.g. 0, false, zero struct) is valid and needs to be distinguished. +func (o Any) IsZero() bool { + return !o.exists +} + +// IsNil is an alias for IsZero. +// +// This method is provided for compatibility with the msgpack Encoder interface. +func (o Any) IsNil() bool { + return o.IsZero() +} + +// Get returns the stored value and a boolean flag indicating its presence. +// If the value is present, returns (value, true). +// If the value is absent, returns (zero value of byte, false). +// +// Recommended usage: +// +// if value, ok := o.Get(); ok { +// // use value +// } +func (o Any) Get() (any, bool) { + return o.value, o.exists +} + +// MustGet returns the stored value if it is present. +// Panics if the value is absent (i.e., IsZero() == true). +// +// Use with caution — only when you are certain the value exists. +// +// Panics with: "optional value is not set" if no value is set. +func (o Any) MustGet() any { + if !o.exists { + panic("optional value is not set!") + } + + return o.value +} + +// Unwrap returns the stored value regardless of presence. +// If no value is set, returns the zero value for byte. +// +// Warning: Does not check presence. Use IsSome() before calling if you need +// to distinguish between absent value and explicit zero value. +func (o Any) Unwrap() any { + return o.value +} + +// UnwrapOr returns the stored value if present. +// Otherwise, returns the provided default value. +// +// Example: +// +// o := NoneAny() +// v := o.UnwrapOr(someDefaultByte) +func (o Any) UnwrapOr(defaultValue any) any { + if o.exists { + return o.value + } + + return defaultValue +} + +// UnwrapOrElse returns the stored value if present. +// Otherwise, calls the provided function and returns its result. +// Useful when the default value requires computation or side effects. +// +// Example: +// +// o := NoneAny() +// v := o.UnwrapOrElse(func() any { return computeDefault() }) +func (o Any) UnwrapOrElse(defaultValue func() any) any { + if o.exists { + return o.value + } + + return defaultValue() +} + +// EncodeMsgpack encodes the Any value using MessagePack format. +// - If the value is present, it is encoded as byte. +// - If the value is absent (None), it is encoded as nil. +// +// Returns an error if encoding fails. +func (o Any) EncodeMsgpack(encoder *msgpack.Encoder) error { + if o.exists { + return newEncodeError("Any", encodeAny(encoder, o.value)) + } + + return newEncodeError("Any", encoder.EncodeNil()) +} + +// DecodeMsgpack decodes a Any value from MessagePack format. +// Supports two input types: +// - nil: interpreted as no value (NoneByte) +// - byte: interpreted as a present value (SomeByte) +// +// Returns an error if the input type is unsupported or decoding fails. +// +// After successful decoding: +// - on nil: exists = false, value = default zero value +// - on any: exists = true, value = decoded value +func (o *Any) DecodeMsgpack(decoder *msgpack.Decoder) error { + code, err := decoder.PeekCode() + if err != nil { + return newDecodeError("Any", err) + } + + switch { + case code == msgpcode.Nil: + o.exists = false + + return newDecodeError("Any", decoder.Skip()) + case checkAny(code): + o.value, err = decodeAny(decoder) + if err != nil { + return newDecodeError("Any", err) + } + + o.exists = true + + return err + default: + return newDecodeWithCodeError("Any", code) + } +} diff --git a/any_gen_test.go b/any_gen_test.go new file mode 100644 index 0000000..9262e9f --- /dev/null +++ b/any_gen_test.go @@ -0,0 +1,238 @@ +package option_test + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/vmihailenco/msgpack/v5" + + "github.com/tarantool/go-option" +) + +func TestAny_IsSome(t *testing.T) { + t.Parallel() + + t.Run("some", func(t *testing.T) { + t.Parallel() + + someAny := option.SomeAny("aaaaaa+++++") + assert.True(t, someAny.IsSome()) + }) + + t.Run("none", func(t *testing.T) { + t.Parallel() + + emptyAny := option.NoneAny() + assert.False(t, emptyAny.IsSome()) + }) +} + +func TestAny_IsZero(t *testing.T) { + t.Parallel() + + t.Run("some", func(t *testing.T) { + t.Parallel() + + someAny := option.SomeAny(12232.777777777) + assert.False(t, someAny.IsZero()) + }) + + t.Run("none", func(t *testing.T) { + t.Parallel() + + emptyAny := option.NoneAny() + assert.True(t, emptyAny.IsZero()) + }) +} + +func TestAny_IsNil(t *testing.T) { + t.Parallel() + + t.Run("some", func(t *testing.T) { + t.Parallel() + + someAny := option.SomeAny(777.111111) + assert.False(t, someAny.IsNil()) + }) + + t.Run("none", func(t *testing.T) { + t.Parallel() + + emptyAny := option.NoneAny() + assert.True(t, emptyAny.IsNil()) + }) +} + +func TestAny_Get(t *testing.T) { + t.Parallel() + + t.Run("some", func(t *testing.T) { + t.Parallel() + + someAny := option.SomeAny("lllllllll") + val, ok := someAny.Get() + require.True(t, ok) + assert.EqualValues(t, "lllllllll", val) + }) + + t.Run("none", func(t *testing.T) { + t.Parallel() + + emptyAny := option.NoneAny() + _, ok := emptyAny.Get() + require.False(t, ok) + }) +} + +func TestAny_MustGet(t *testing.T) { + t.Parallel() + + t.Run("some", func(t *testing.T) { + t.Parallel() + + someAny := option.SomeAny(1111.1000000) + assert.InEpsilon(t, 1111.1000000, someAny.MustGet(), 0.01) + }) + + t.Run("none", func(t *testing.T) { + t.Parallel() + + emptyAny := option.NoneAny() + + assert.Panics(t, func() { + emptyAny.MustGet() + }) + }) +} + +func TestAny_Unwrap(t *testing.T) { + t.Parallel() + + t.Run("some", func(t *testing.T) { + t.Parallel() + + someAny := option.SomeAny( + "HH77771111111111111111111111111111111111111111111111111111111111111111111111") + assert.EqualValues(t, + "HH77771111111111111111111111111111111111111111111111111111111111111111111111", + someAny.Unwrap()) + }) + + t.Run("none", func(t *testing.T) { + t.Parallel() + + emptyAny := option.NoneAny() + + assert.NotPanics(t, func() { + emptyAny.Unwrap() + }) + }) +} + +func TestAny_UnwrapOr(t *testing.T) { + t.Parallel() + + t.Run("some", func(t *testing.T) { + t.Parallel() + + someAny := option.SomeAny("(((09,111") + assert.EqualValues(t, "(((09,111", someAny.UnwrapOr(111111)) + }) + + t.Run("none", func(t *testing.T) { + t.Parallel() + + emptyAny := option.NoneAny() + assert.InEpsilon(t, 11111.8880, emptyAny.UnwrapOr(11111.8880), 0.01) + }) +} + +func TestAny_UnwrapOrElse(t *testing.T) { + t.Parallel() + + t.Run("some", func(t *testing.T) { + t.Parallel() + + someAny := option.SomeAny(34534534) + assert.EqualValues(t, 34534534, someAny.UnwrapOrElse(func() any { + return "EXAMPLE!!!" + })) + }) + + t.Run("none", func(t *testing.T) { + t.Parallel() + + emptyAny := option.NoneAny() + assert.EqualValues(t, 145, emptyAny.UnwrapOrElse(func() any { + return 145 + })) + }) +} + +func TestAny_EncodeDecodeMsgpack(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + value any + expected any + }{ + {"string", "test string", "test string"}, + {"int", 42, int64(42)}, + {"float", 3.14, 3.14}, + {"bool", true, true}, + {"slice", []int{1, 2, 3}, []any{int8(1), int8(2), int8(3)}}, + {"map", map[string]int{"a": 1}, map[string]any{"a": int8(1)}}, + } + + for _, testCase := range testCases { + t.Run("some_"+testCase.name, func(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + // Encode. + someAny := option.SomeAny(testCase.value) + err := someAny.EncodeMsgpack(enc) + require.NoError(t, err) + + // Decode. + var unmarshaled option.Any + + err = unmarshaled.DecodeMsgpack(dec) + require.NoError(t, err) + assert.True(t, unmarshaled.IsSome()) + assert.Equal(t, testCase.expected, unmarshaled.Unwrap()) + }) + } + + t.Run("none", func(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + // Encode nil. + emptyAny := option.NoneAny() + + err := emptyAny.EncodeMsgpack(enc) + require.NoError(t, err) + + // Decode. + var unmarshaled option.Any + + err = unmarshaled.DecodeMsgpack(dec) + require.NoError(t, err) + + // Verify it's none. + assert.False(t, unmarshaled.IsSome()) + assert.Nil(t, unmarshaled.Unwrap()) + }) +} diff --git a/cmd/generator/generator.go b/cmd/generator/generator.go index 857ab24..1a47b73 100644 --- a/cmd/generator/generator.go +++ b/cmd/generator/generator.go @@ -34,10 +34,10 @@ type generatorDef struct { UnexpectedTestingValue string } -func structToMap(def generatorDef) map[string]interface{} { +func structToMap(def generatorDef) map[string]any { caser := cases.Title(language.English) - out := map[string]interface{}{ + out := map[string]any{ "Name": caser.String(def.Name), "Type": def.Name, "DecodeFunc": def.DecodeFunc, diff --git a/cmd/gentypes/main.go b/cmd/gentypes/main.go index d4a9dd9..3c0ff9f 100644 --- a/cmd/gentypes/main.go +++ b/cmd/gentypes/main.go @@ -33,7 +33,7 @@ var ( customUnmarshalFunc string ) -func logfuncf(format string, args ...interface{}) { +func logfuncf(format string, args ...any) { if verbose { fmt.Printf("> "+format+"\n", args...) } diff --git a/generic.go b/generic.go index 268bd81..b329ef8 100644 --- a/generic.go +++ b/generic.go @@ -161,7 +161,7 @@ func (o Generic[T]) UnwrapOrElse(defaultValueFunc func() T) T { // convertToEncoder checks whether the given value implements msgpack.CustomEncoder. // // Used internally during encoding to support custom MessagePack encoding logic. -func convertToEncoder(v interface{}) (msgpack.CustomEncoder, bool) { +func convertToEncoder(v any) (msgpack.CustomEncoder, bool) { enc, ok := v.(msgpack.CustomEncoder) return enc, ok } @@ -196,7 +196,7 @@ func (o Generic[T]) EncodeMsgpack(encoder *msgpack.Encoder) error { // convertToDecoder checks whether the given value implements msgpack.CustomDecoder. // // Used internally during decoding to support custom MessagePack decoding logic. -func convertToDecoder(v interface{}) (msgpack.CustomDecoder, bool) { +func convertToDecoder(v any) (msgpack.CustomDecoder, bool) { dec, ok := v.(msgpack.CustomDecoder) return dec, ok } diff --git a/msgpack.go b/msgpack.go index 263ce5f..ea620af 100644 --- a/msgpack.go +++ b/msgpack.go @@ -167,3 +167,14 @@ func decodeByte(decoder *msgpack.Decoder) (byte, error) { func encodeByte(encoder *msgpack.Encoder, b byte) error { return encoder.EncodeUint8(b) //nolint:wrapcheck } + +func checkAny(code byte) bool { + return code != msgpcode.Nil +} +func encodeAny(encoder *msgpack.Encoder, val any) error { + return encoder.Encode(val) //nolint:wrapcheck +} + +func decodeAny(decoder *msgpack.Decoder) (any, error) { + return decoder.DecodeInterfaceLoose() //nolint:wrapcheck +}