diff --git a/arrow/arrow.go b/arrow/arrow.go index aaeaccca..a7767c45 100644 --- a/arrow/arrow.go +++ b/arrow/arrow.go @@ -7,6 +7,8 @@ import ( "github.com/vmihailenco/msgpack/v5" ) +//go:generate go tool gentypes -ext-code 8 Arrow + // Arrow MessagePack extension type. const arrowExtId = 8 @@ -26,6 +28,17 @@ func (a Arrow) Raw() []byte { return a.data } +// MarshalMsgpack implements a custom msgpack marshaler for extension type. +func (a Arrow) MarshalMsgpack() ([]byte, error) { + return a.data, nil +} + +// UnmarshalMsgpack implements a custom msgpack unmarshaler for extension type. +func (a *Arrow) UnmarshalMsgpack(data []byte) error { + a.data = data + return nil +} + func arrowDecoder(d *msgpack.Decoder, v reflect.Value, extLen int) error { arrow := Arrow{ data: make([]byte, extLen), diff --git a/arrow/arrow_gen.go b/arrow/arrow_gen.go new file mode 100644 index 00000000..c86c7277 --- /dev/null +++ b/arrow/arrow_gen.go @@ -0,0 +1,241 @@ +// Code generated by github.com/tarantool/go-option; DO NOT EDIT. + +package arrow + +import ( + "fmt" + + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" + + "github.com/tarantool/go-option" +) + +// OptionalArrow represents an optional value of type Arrow. +// It can either hold a valid Arrow (IsSome == true) or be empty (IsZero == true). +type OptionalArrow struct { + value Arrow + exists bool +} + +// SomeOptionalArrow creates an optional OptionalArrow with the given Arrow value. +// The returned OptionalArrow will have IsSome() == true and IsZero() == false. +func SomeOptionalArrow(value Arrow) OptionalArrow { + return OptionalArrow{ + value: value, + exists: true, + } +} + +// NoneOptionalArrow creates an empty optional OptionalArrow value. +// The returned OptionalArrow will have IsSome() == false and IsZero() == true. +// +// Example: +// +// o := NoneOptionalArrow() +// if o.IsZero() { +// fmt.Println("value is absent") +// } +func NoneOptionalArrow() OptionalArrow { + return OptionalArrow{} +} + +func (o OptionalArrow) newEncodeError(err error) error { + if err == nil { + return nil + } + return &option.EncodeError{ + Type: "OptionalArrow", + Parent: err, + } +} + +func (o OptionalArrow) newDecodeError(err error) error { + if err == nil { + return nil + } + + return &option.DecodeError{ + Type: "OptionalArrow", + Parent: err, + } +} + +// IsSome returns true if the OptionalArrow contains a value. +// This indicates the value is explicitly set (not None). +func (o OptionalArrow) IsSome() bool { + return o.exists +} + +// IsZero returns true if the OptionalArrow 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 OptionalArrow) IsZero() bool { + return !o.exists +} + +// IsNil is an alias for IsZero. +// +// This method is provided for compatibility with the msgpack Encoder interface. +func (o OptionalArrow) 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 Arrow, false). +// +// Recommended usage: +// +// if value, ok := o.Get(); ok { +// // use value +// } +func (o OptionalArrow) Get() (Arrow, 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 OptionalArrow) MustGet() Arrow { + 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 Arrow. +// +// Warning: Does not check presence. Use IsSome() before calling if you need +// to distinguish between absent value and explicit zero value. +func (o OptionalArrow) Unwrap() Arrow { + return o.value +} + +// UnwrapOr returns the stored value if present. +// Otherwise, returns the provided default value. +// +// Example: +// +// o := NoneOptionalArrow() +// v := o.UnwrapOr(someDefaultOptionalArrow) +func (o OptionalArrow) UnwrapOr(defaultValue Arrow) Arrow { + 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 := NoneOptionalArrow() +// v := o.UnwrapOrElse(func() Arrow { return computeDefault() }) +func (o OptionalArrow) UnwrapOrElse(defaultValue func() Arrow) Arrow { + if o.exists { + return o.value + } + + return defaultValue() +} + +func (o OptionalArrow) encodeValue(encoder *msgpack.Encoder) error { + value, err := o.value.MarshalMsgpack() + if err != nil { + return err + } + + err = encoder.EncodeExtHeader(8, len(value)) + if err != nil { + return err + } + + _, err = encoder.Writer().Write(value) + if err != nil { + return err + } + + return nil +} + +// EncodeMsgpack encodes the OptionalArrow value using MessagePack format. +// - If the value is present, it is encoded as Arrow. +// - If the value is absent (None), it is encoded as nil. +// +// Returns an error if encoding fails. +func (o OptionalArrow) EncodeMsgpack(encoder *msgpack.Encoder) error { + if o.exists { + return o.newEncodeError(o.encodeValue(encoder)) + } + + return o.newEncodeError(encoder.EncodeNil()) +} + +func (o *OptionalArrow) decodeValue(decoder *msgpack.Decoder) error { + tp, length, err := decoder.DecodeExtHeader() + switch { + case err != nil: + return o.newDecodeError(err) + case tp != 8: + return o.newDecodeError(fmt.Errorf("invalid extension code: %d", tp)) + } + + a := make([]byte, length) + if err := decoder.ReadFull(a); err != nil { + return o.newDecodeError(err) + } + + if err := o.value.UnmarshalMsgpack(a); err != nil { + return o.newDecodeError(err) + } + + o.exists = true + return nil +} + +func (o *OptionalArrow) checkCode(code byte) bool { + return msgpcode.IsExt(code) +} + +// DecodeMsgpack decodes a OptionalArrow value from MessagePack format. +// Supports two input types: +// - nil: interpreted as no value (NoneOptionalArrow) +// - Arrow: interpreted as a present value (SomeOptionalArrow) +// +// Returns an error if the input type is unsupported or decoding fails. +// +// After successful decoding: +// - on nil: exists = false, value = default zero value +// - on Arrow: exists = true, value = decoded value +func (o *OptionalArrow) DecodeMsgpack(decoder *msgpack.Decoder) error { + code, err := decoder.PeekCode() + if err != nil { + return o.newDecodeError(err) + } + + switch { + case code == msgpcode.Nil: + o.exists = false + + return o.newDecodeError(decoder.Skip()) + case o.checkCode(code): + err := o.decodeValue(decoder) + if err != nil { + return o.newDecodeError(err) + } + o.exists = true + + return err + default: + return o.newDecodeError(fmt.Errorf("unexpected code: %d", code)) + } +} diff --git a/arrow/arrow_gen_test.go b/arrow/arrow_gen_test.go new file mode 100644 index 00000000..d990499f --- /dev/null +++ b/arrow/arrow_gen_test.go @@ -0,0 +1,124 @@ +package arrow + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/vmihailenco/msgpack/v5" +) + +func TestSomeOptionalArrow(t *testing.T) { + val, err := MakeArrow([]byte{1, 2, 3}) + assert.NoError(t, err) + opt := SomeOptionalArrow(val) + + assert.True(t, opt.IsSome()) + assert.False(t, opt.IsZero()) + + v, ok := opt.Get() + assert.True(t, ok) + assert.Equal(t, val, v) +} + +func TestNoneOptionalArrow(t *testing.T) { + opt := NoneOptionalArrow() + + assert.False(t, opt.IsSome()) + assert.True(t, opt.IsZero()) + + _, ok := opt.Get() + assert.False(t, ok) +} + +func TestOptionalArrow_MustGet(t *testing.T) { + val, err := MakeArrow([]byte{1, 2, 3}) + assert.NoError(t, err) + optSome := SomeOptionalArrow(val) + optNone := NoneOptionalArrow() + + assert.Equal(t, val, optSome.MustGet()) + assert.Panics(t, func() { optNone.MustGet() }) +} + +func TestOptionalArrow_Unwrap(t *testing.T) { + val, err := MakeArrow([]byte{1, 2, 3}) + assert.NoError(t, err) + optSome := SomeOptionalArrow(val) + optNone := NoneOptionalArrow() + + assert.Equal(t, val, optSome.Unwrap()) + assert.Equal(t, Arrow{}, optNone.Unwrap()) +} + +func TestOptionalArrow_UnwrapOr(t *testing.T) { + val, err := MakeArrow([]byte{1, 2, 3}) + assert.NoError(t, err) + def, err := MakeArrow([]byte{4, 5, 6}) + assert.NoError(t, err) + optSome := SomeOptionalArrow(val) + optNone := NoneOptionalArrow() + + assert.Equal(t, val, optSome.UnwrapOr(def)) + assert.Equal(t, def, optNone.UnwrapOr(def)) +} + +func TestOptionalArrow_UnwrapOrElse(t *testing.T) { + val, err := MakeArrow([]byte{1, 2, 3}) + assert.NoError(t, err) + def, err := MakeArrow([]byte{4, 5, 6}) + assert.NoError(t, err) + optSome := SomeOptionalArrow(val) + optNone := NoneOptionalArrow() + + assert.Equal(t, val, optSome.UnwrapOrElse(func() Arrow { return def })) + assert.Equal(t, def, optNone.UnwrapOrElse(func() Arrow { return def })) +} + +func TestOptionalArrow_EncodeDecodeMsgpack_Some(t *testing.T) { + val, err := MakeArrow([]byte{1, 2, 3}) + assert.NoError(t, err) + some := SomeOptionalArrow(val) + + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err = enc.Encode(some) + assert.NoError(t, err) + + var decodedSome OptionalArrow + err = dec.Decode(&decodedSome) + assert.NoError(t, err) + assert.True(t, decodedSome.IsSome()) + assert.Equal(t, val, decodedSome.Unwrap()) +} + +func TestOptionalArrow_EncodeDecodeMsgpack_None(t *testing.T) { + none := NoneOptionalArrow() + + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(none) + assert.NoError(t, err) + + var decodedNone OptionalArrow + err = dec.Decode(&decodedNone) + assert.NoError(t, err) + assert.True(t, decodedNone.IsZero()) +} + +func TestOptionalArrow_EncodeDecodeMsgpack_InvalidType(t *testing.T) { + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(123) + assert.NoError(t, err) + + var decodedInvalid OptionalArrow + err = dec.Decode(&decodedInvalid) + assert.Error(t, err) +} diff --git a/box_error.go b/boxerror.go similarity index 99% rename from box_error.go rename to boxerror.go index 59bfb4a0..2fb18268 100644 --- a/box_error.go +++ b/boxerror.go @@ -9,6 +9,8 @@ import ( const errorExtID = 3 +//go:generate go tool gentypes -ext-code 3 BoxError + const ( keyErrorStack = 0x00 keyErrorType = 0x00 diff --git a/boxerror_gen.go b/boxerror_gen.go new file mode 100644 index 00000000..07a1be69 --- /dev/null +++ b/boxerror_gen.go @@ -0,0 +1,241 @@ +// Code generated by github.com/tarantool/go-option; DO NOT EDIT. + +package tarantool + +import ( + "fmt" + + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" + + "github.com/tarantool/go-option" +) + +// OptionalBoxError represents an optional value of type BoxError. +// It can either hold a valid BoxError (IsSome == true) or be empty (IsZero == true). +type OptionalBoxError struct { + value BoxError + exists bool +} + +// SomeOptionalBoxError creates an optional OptionalBoxError with the given BoxError value. +// The returned OptionalBoxError will have IsSome() == true and IsZero() == false. +func SomeOptionalBoxError(value BoxError) OptionalBoxError { + return OptionalBoxError{ + value: value, + exists: true, + } +} + +// NoneOptionalBoxError creates an empty optional OptionalBoxError value. +// The returned OptionalBoxError will have IsSome() == false and IsZero() == true. +// +// Example: +// +// o := NoneOptionalBoxError() +// if o.IsZero() { +// fmt.Println("value is absent") +// } +func NoneOptionalBoxError() OptionalBoxError { + return OptionalBoxError{} +} + +func (o OptionalBoxError) newEncodeError(err error) error { + if err == nil { + return nil + } + return &option.EncodeError{ + Type: "OptionalBoxError", + Parent: err, + } +} + +func (o OptionalBoxError) newDecodeError(err error) error { + if err == nil { + return nil + } + + return &option.DecodeError{ + Type: "OptionalBoxError", + Parent: err, + } +} + +// IsSome returns true if the OptionalBoxError contains a value. +// This indicates the value is explicitly set (not None). +func (o OptionalBoxError) IsSome() bool { + return o.exists +} + +// IsZero returns true if the OptionalBoxError 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 OptionalBoxError) IsZero() bool { + return !o.exists +} + +// IsNil is an alias for IsZero. +// +// This method is provided for compatibility with the msgpack Encoder interface. +func (o OptionalBoxError) 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 BoxError, false). +// +// Recommended usage: +// +// if value, ok := o.Get(); ok { +// // use value +// } +func (o OptionalBoxError) Get() (BoxError, 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 OptionalBoxError) MustGet() BoxError { + 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 BoxError. +// +// Warning: Does not check presence. Use IsSome() before calling if you need +// to distinguish between absent value and explicit zero value. +func (o OptionalBoxError) Unwrap() BoxError { + return o.value +} + +// UnwrapOr returns the stored value if present. +// Otherwise, returns the provided default value. +// +// Example: +// +// o := NoneOptionalBoxError() +// v := o.UnwrapOr(someDefaultOptionalBoxError) +func (o OptionalBoxError) UnwrapOr(defaultValue BoxError) BoxError { + 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 := NoneOptionalBoxError() +// v := o.UnwrapOrElse(func() BoxError { return computeDefault() }) +func (o OptionalBoxError) UnwrapOrElse(defaultValue func() BoxError) BoxError { + if o.exists { + return o.value + } + + return defaultValue() +} + +func (o OptionalBoxError) encodeValue(encoder *msgpack.Encoder) error { + value, err := o.value.MarshalMsgpack() + if err != nil { + return err + } + + err = encoder.EncodeExtHeader(3, len(value)) + if err != nil { + return err + } + + _, err = encoder.Writer().Write(value) + if err != nil { + return err + } + + return nil +} + +// EncodeMsgpack encodes the OptionalBoxError value using MessagePack format. +// - If the value is present, it is encoded as BoxError. +// - If the value is absent (None), it is encoded as nil. +// +// Returns an error if encoding fails. +func (o OptionalBoxError) EncodeMsgpack(encoder *msgpack.Encoder) error { + if o.exists { + return o.newEncodeError(o.encodeValue(encoder)) + } + + return o.newEncodeError(encoder.EncodeNil()) +} + +func (o *OptionalBoxError) decodeValue(decoder *msgpack.Decoder) error { + tp, length, err := decoder.DecodeExtHeader() + switch { + case err != nil: + return o.newDecodeError(err) + case tp != 3: + return o.newDecodeError(fmt.Errorf("invalid extension code: %d", tp)) + } + + a := make([]byte, length) + if err := decoder.ReadFull(a); err != nil { + return o.newDecodeError(err) + } + + if err := o.value.UnmarshalMsgpack(a); err != nil { + return o.newDecodeError(err) + } + + o.exists = true + return nil +} + +func (o *OptionalBoxError) checkCode(code byte) bool { + return msgpcode.IsExt(code) +} + +// DecodeMsgpack decodes a OptionalBoxError value from MessagePack format. +// Supports two input types: +// - nil: interpreted as no value (NoneOptionalBoxError) +// - BoxError: interpreted as a present value (SomeOptionalBoxError) +// +// Returns an error if the input type is unsupported or decoding fails. +// +// After successful decoding: +// - on nil: exists = false, value = default zero value +// - on BoxError: exists = true, value = decoded value +func (o *OptionalBoxError) DecodeMsgpack(decoder *msgpack.Decoder) error { + code, err := decoder.PeekCode() + if err != nil { + return o.newDecodeError(err) + } + + switch { + case code == msgpcode.Nil: + o.exists = false + + return o.newDecodeError(decoder.Skip()) + case o.checkCode(code): + err := o.decodeValue(decoder) + if err != nil { + return o.newDecodeError(err) + } + o.exists = true + + return err + default: + return o.newDecodeError(fmt.Errorf("unexpected code: %d", code)) + } +} diff --git a/boxerror_gen_test.go b/boxerror_gen_test.go new file mode 100644 index 00000000..4d7fada3 --- /dev/null +++ b/boxerror_gen_test.go @@ -0,0 +1,116 @@ +package tarantool + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/vmihailenco/msgpack/v5" +) + +func TestSomeOptionalBoxError(t *testing.T) { + val := BoxError{Code: 1, Msg: "error"} + opt := SomeOptionalBoxError(val) + + assert.True(t, opt.IsSome()) + assert.False(t, opt.IsZero()) + + v, ok := opt.Get() + assert.True(t, ok) + assert.Equal(t, val, v) +} + +func TestNoneOptionalBoxError(t *testing.T) { + opt := NoneOptionalBoxError() + + assert.False(t, opt.IsSome()) + assert.True(t, opt.IsZero()) + + _, ok := opt.Get() + assert.False(t, ok) +} + +func TestOptionalBoxError_MustGet(t *testing.T) { + val := BoxError{Code: 1, Msg: "error"} + optSome := SomeOptionalBoxError(val) + optNone := NoneOptionalBoxError() + + assert.Equal(t, val, optSome.MustGet()) + assert.Panics(t, func() { optNone.MustGet() }) +} + +func TestOptionalBoxError_Unwrap(t *testing.T) { + val := BoxError{Code: 1, Msg: "error"} + optSome := SomeOptionalBoxError(val) + optNone := NoneOptionalBoxError() + + assert.Equal(t, val, optSome.Unwrap()) + assert.Equal(t, BoxError{}, optNone.Unwrap()) +} + +func TestOptionalBoxError_UnwrapOr(t *testing.T) { + val := BoxError{Code: 1, Msg: "error"} + def := BoxError{Code: 2, Msg: "default"} + optSome := SomeOptionalBoxError(val) + optNone := NoneOptionalBoxError() + + assert.Equal(t, val, optSome.UnwrapOr(def)) + assert.Equal(t, def, optNone.UnwrapOr(def)) +} + +func TestOptionalBoxError_UnwrapOrElse(t *testing.T) { + val := BoxError{Code: 1, Msg: "error"} + def := BoxError{Code: 2, Msg: "default"} + optSome := SomeOptionalBoxError(val) + optNone := NoneOptionalBoxError() + + assert.Equal(t, val, optSome.UnwrapOrElse(func() BoxError { return def })) + assert.Equal(t, def, optNone.UnwrapOrElse(func() BoxError { return def })) +} + +func TestOptionalBoxError_EncodeDecodeMsgpack_Some(t *testing.T) { + val := BoxError{Code: 1, Msg: "error"} + some := SomeOptionalBoxError(val) + + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(some) + assert.NoError(t, err) + + var decodedSome OptionalBoxError + err = dec.Decode(&decodedSome) + assert.NoError(t, err) + assert.True(t, decodedSome.IsSome()) + assert.Equal(t, val, decodedSome.Unwrap()) +} + +func TestOptionalBoxError_EncodeDecodeMsgpack_None(t *testing.T) { + none := NoneOptionalBoxError() + + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(none) + assert.NoError(t, err) + + var decodedNone OptionalBoxError + err = dec.Decode(&decodedNone) + assert.NoError(t, err) + assert.True(t, decodedNone.IsZero()) +} + +func TestOptionalBoxError_EncodeDecodeMsgpack_InvalidType(t *testing.T) { + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(123) + assert.NoError(t, err) + + var decodedInvalid OptionalBoxError + err = dec.Decode(&decodedInvalid) + assert.Error(t, err) +} diff --git a/box_error_test.go b/boxerror_test.go similarity index 100% rename from box_error_test.go rename to boxerror_test.go diff --git a/datetime/datetime.go b/datetime/datetime.go index f5a2a827..23901305 100644 --- a/datetime/datetime.go +++ b/datetime/datetime.go @@ -76,6 +76,7 @@ const ( const maxSize = secondsSize + nsecSize + tzIndexSize + tzOffsetSize +//go:generate go tool gentypes -ext-code 4 Datetime type Datetime struct { time time.Time } @@ -183,6 +184,89 @@ func addMonth(ival Interval, delta int64, adjust Adjust) Interval { return ival } +// MarshalMsgpack implements a custom msgpack marshaler. +func (d Datetime) MarshalMsgpack() ([]byte, error) { + tm := d.ToTime() + + var dt datetime + dt.seconds = tm.Unix() + dt.nsec = int32(tm.Nanosecond()) + + zone := tm.Location().String() + _, offset := tm.Zone() + if zone != NoTimezone { + // The zone value already checked in MakeDatetime() or + // UnmarshalMsgpack() calls. + dt.tzIndex = int16(timezoneToIndex[zone]) + } + dt.tzOffset = int16(offset / 60) + + var bytesSize = secondsSize + if dt.nsec != 0 || dt.tzOffset != 0 || dt.tzIndex != 0 { + bytesSize += nsecSize + tzIndexSize + tzOffsetSize + } + + buf := make([]byte, bytesSize) + binary.LittleEndian.PutUint64(buf, uint64(dt.seconds)) + if bytesSize == maxSize { + binary.LittleEndian.PutUint32(buf[secondsSize:], uint32(dt.nsec)) + binary.LittleEndian.PutUint16(buf[secondsSize+nsecSize:], uint16(dt.tzOffset)) + binary.LittleEndian.PutUint16(buf[secondsSize+nsecSize+tzOffsetSize:], uint16(dt.tzIndex)) + } + + return buf, nil +} + +// UnmarshalMsgpack implements a custom msgpack unmarshaler. +func (d *Datetime) UnmarshalMsgpack(data []byte) error { + var dt datetime + + sec := binary.LittleEndian.Uint64(data) + dt.seconds = int64(sec) + dt.nsec = 0 + if len(data) == maxSize { + dt.nsec = int32(binary.LittleEndian.Uint32(data[secondsSize:])) + dt.tzOffset = int16(binary.LittleEndian.Uint16(data[secondsSize+nsecSize:])) + dt.tzIndex = int16(binary.LittleEndian.Uint16(data[secondsSize+nsecSize+tzOffsetSize:])) + } + + tt := time.Unix(dt.seconds, int64(dt.nsec)) + + loc := noTimezoneLoc + if dt.tzIndex != 0 || dt.tzOffset != 0 { + zone := NoTimezone + offset := int(dt.tzOffset) * 60 + + if dt.tzIndex != 0 { + if _, ok := indexToTimezone[int(dt.tzIndex)]; !ok { + return fmt.Errorf("unknown timezone index %d", dt.tzIndex) + } + zone = indexToTimezone[int(dt.tzIndex)] + } + if zone != NoTimezone { + if loadLoc, err := time.LoadLocation(zone); err == nil { + loc = loadLoc + } else { + // Unable to load location. + loc = time.FixedZone(zone, offset) + } + } else { + // Only offset. + loc = time.FixedZone(zone, offset) + } + } + tt = tt.In(loc) + + newDatetime, err := MakeDatetime(tt) + if err != nil { + return err + } + + *d = newDatetime + + return nil +} + func (d Datetime) add(ival Interval, positive bool) (Datetime, error) { newVal := intervalFromDatetime(d) @@ -244,35 +328,8 @@ func (d *Datetime) ToTime() time.Time { func datetimeEncoder(e *msgpack.Encoder, v reflect.Value) ([]byte, error) { dtime := v.Interface().(Datetime) - tm := dtime.ToTime() - - var dt datetime - dt.seconds = tm.Unix() - dt.nsec = int32(tm.Nanosecond()) - - zone := tm.Location().String() - _, offset := tm.Zone() - if zone != NoTimezone { - // The zone value already checked in MakeDatetime() or - // UnmarshalMsgpack() calls. - dt.tzIndex = int16(timezoneToIndex[zone]) - } - dt.tzOffset = int16(offset / 60) - var bytesSize = secondsSize - if dt.nsec != 0 || dt.tzOffset != 0 || dt.tzIndex != 0 { - bytesSize += nsecSize + tzIndexSize + tzOffsetSize - } - - buf := make([]byte, bytesSize) - binary.LittleEndian.PutUint64(buf, uint64(dt.seconds)) - if bytesSize == maxSize { - binary.LittleEndian.PutUint32(buf[secondsSize:], uint32(dt.nsec)) - binary.LittleEndian.PutUint16(buf[secondsSize+nsecSize:], uint16(dt.tzOffset)) - binary.LittleEndian.PutUint16(buf[secondsSize+nsecSize+tzOffsetSize:], uint16(dt.tzIndex)) - } - - return buf, nil + return dtime.MarshalMsgpack() } func datetimeDecoder(d *msgpack.Decoder, v reflect.Value, extLen int) error { @@ -282,54 +339,15 @@ func datetimeDecoder(d *msgpack.Decoder, v reflect.Value, extLen int) error { } b := make([]byte, extLen) - n, err := d.Buffered().Read(b) - if err != nil { + switch n, err := d.Buffered().Read(b); { + case err != nil: return err - } - if n < extLen { + case n < extLen: return fmt.Errorf("msgpack: unexpected end of stream after %d datetime bytes", n) } - var dt datetime - sec := binary.LittleEndian.Uint64(b) - dt.seconds = int64(sec) - dt.nsec = 0 - if extLen == maxSize { - dt.nsec = int32(binary.LittleEndian.Uint32(b[secondsSize:])) - dt.tzOffset = int16(binary.LittleEndian.Uint16(b[secondsSize+nsecSize:])) - dt.tzIndex = int16(binary.LittleEndian.Uint16(b[secondsSize+nsecSize+tzOffsetSize:])) - } - - tt := time.Unix(dt.seconds, int64(dt.nsec)) - - loc := noTimezoneLoc - if dt.tzIndex != 0 || dt.tzOffset != 0 { - zone := NoTimezone - offset := int(dt.tzOffset) * 60 - - if dt.tzIndex != 0 { - if _, ok := indexToTimezone[int(dt.tzIndex)]; !ok { - return fmt.Errorf("unknown timezone index %d", dt.tzIndex) - } - zone = indexToTimezone[int(dt.tzIndex)] - } - if zone != NoTimezone { - if loadLoc, err := time.LoadLocation(zone); err == nil { - loc = loadLoc - } else { - // Unable to load location. - loc = time.FixedZone(zone, offset) - } - } else { - // Only offset. - loc = time.FixedZone(zone, offset) - } - } - tt = tt.In(loc) - ptr := v.Addr().Interface().(*Datetime) - *ptr, err = MakeDatetime(tt) - return err + return ptr.UnmarshalMsgpack(b) } func init() { diff --git a/datetime/datetime_gen.go b/datetime/datetime_gen.go new file mode 100644 index 00000000..753d9c37 --- /dev/null +++ b/datetime/datetime_gen.go @@ -0,0 +1,241 @@ +// Code generated by github.com/tarantool/go-option; DO NOT EDIT. + +package datetime + +import ( + "fmt" + + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" + + "github.com/tarantool/go-option" +) + +// OptionalDatetime represents an optional value of type Datetime. +// It can either hold a valid Datetime (IsSome == true) or be empty (IsZero == true). +type OptionalDatetime struct { + value Datetime + exists bool +} + +// SomeOptionalDatetime creates an optional OptionalDatetime with the given Datetime value. +// The returned OptionalDatetime will have IsSome() == true and IsZero() == false. +func SomeOptionalDatetime(value Datetime) OptionalDatetime { + return OptionalDatetime{ + value: value, + exists: true, + } +} + +// NoneOptionalDatetime creates an empty optional OptionalDatetime value. +// The returned OptionalDatetime will have IsSome() == false and IsZero() == true. +// +// Example: +// +// o := NoneOptionalDatetime() +// if o.IsZero() { +// fmt.Println("value is absent") +// } +func NoneOptionalDatetime() OptionalDatetime { + return OptionalDatetime{} +} + +func (o OptionalDatetime) newEncodeError(err error) error { + if err == nil { + return nil + } + return &option.EncodeError{ + Type: "OptionalDatetime", + Parent: err, + } +} + +func (o OptionalDatetime) newDecodeError(err error) error { + if err == nil { + return nil + } + + return &option.DecodeError{ + Type: "OptionalDatetime", + Parent: err, + } +} + +// IsSome returns true if the OptionalDatetime contains a value. +// This indicates the value is explicitly set (not None). +func (o OptionalDatetime) IsSome() bool { + return o.exists +} + +// IsZero returns true if the OptionalDatetime 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 OptionalDatetime) IsZero() bool { + return !o.exists +} + +// IsNil is an alias for IsZero. +// +// This method is provided for compatibility with the msgpack Encoder interface. +func (o OptionalDatetime) 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 Datetime, false). +// +// Recommended usage: +// +// if value, ok := o.Get(); ok { +// // use value +// } +func (o OptionalDatetime) Get() (Datetime, 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 OptionalDatetime) MustGet() Datetime { + 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 Datetime. +// +// Warning: Does not check presence. Use IsSome() before calling if you need +// to distinguish between absent value and explicit zero value. +func (o OptionalDatetime) Unwrap() Datetime { + return o.value +} + +// UnwrapOr returns the stored value if present. +// Otherwise, returns the provided default value. +// +// Example: +// +// o := NoneOptionalDatetime() +// v := o.UnwrapOr(someDefaultOptionalDatetime) +func (o OptionalDatetime) UnwrapOr(defaultValue Datetime) Datetime { + 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 := NoneOptionalDatetime() +// v := o.UnwrapOrElse(func() Datetime { return computeDefault() }) +func (o OptionalDatetime) UnwrapOrElse(defaultValue func() Datetime) Datetime { + if o.exists { + return o.value + } + + return defaultValue() +} + +func (o OptionalDatetime) encodeValue(encoder *msgpack.Encoder) error { + value, err := o.value.MarshalMsgpack() + if err != nil { + return err + } + + err = encoder.EncodeExtHeader(4, len(value)) + if err != nil { + return err + } + + _, err = encoder.Writer().Write(value) + if err != nil { + return err + } + + return nil +} + +// EncodeMsgpack encodes the OptionalDatetime value using MessagePack format. +// - If the value is present, it is encoded as Datetime. +// - If the value is absent (None), it is encoded as nil. +// +// Returns an error if encoding fails. +func (o OptionalDatetime) EncodeMsgpack(encoder *msgpack.Encoder) error { + if o.exists { + return o.newEncodeError(o.encodeValue(encoder)) + } + + return o.newEncodeError(encoder.EncodeNil()) +} + +func (o *OptionalDatetime) decodeValue(decoder *msgpack.Decoder) error { + tp, length, err := decoder.DecodeExtHeader() + switch { + case err != nil: + return o.newDecodeError(err) + case tp != 4: + return o.newDecodeError(fmt.Errorf("invalid extension code: %d", tp)) + } + + a := make([]byte, length) + if err := decoder.ReadFull(a); err != nil { + return o.newDecodeError(err) + } + + if err := o.value.UnmarshalMsgpack(a); err != nil { + return o.newDecodeError(err) + } + + o.exists = true + return nil +} + +func (o *OptionalDatetime) checkCode(code byte) bool { + return msgpcode.IsExt(code) +} + +// DecodeMsgpack decodes a OptionalDatetime value from MessagePack format. +// Supports two input types: +// - nil: interpreted as no value (NoneOptionalDatetime) +// - Datetime: interpreted as a present value (SomeOptionalDatetime) +// +// Returns an error if the input type is unsupported or decoding fails. +// +// After successful decoding: +// - on nil: exists = false, value = default zero value +// - on Datetime: exists = true, value = decoded value +func (o *OptionalDatetime) DecodeMsgpack(decoder *msgpack.Decoder) error { + code, err := decoder.PeekCode() + if err != nil { + return o.newDecodeError(err) + } + + switch { + case code == msgpcode.Nil: + o.exists = false + + return o.newDecodeError(decoder.Skip()) + case o.checkCode(code): + err := o.decodeValue(decoder) + if err != nil { + return o.newDecodeError(err) + } + o.exists = true + + return err + default: + return o.newDecodeError(fmt.Errorf("unexpected code: %d", code)) + } +} diff --git a/datetime/datetime_gen_test.go b/datetime/datetime_gen_test.go new file mode 100644 index 00000000..6b45b6fb --- /dev/null +++ b/datetime/datetime_gen_test.go @@ -0,0 +1,125 @@ +package datetime + +import ( + "bytes" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/vmihailenco/msgpack/v5" +) + +func TestSomeOptionalDatetime(t *testing.T) { + val, err := MakeDatetime(time.Now().In(time.UTC)) + assert.NoError(t, err) + opt := SomeOptionalDatetime(val) + + assert.True(t, opt.IsSome()) + assert.False(t, opt.IsZero()) + + v, ok := opt.Get() + assert.True(t, ok) + assert.Equal(t, val, v) +} + +func TestNoneOptionalDatetime(t *testing.T) { + opt := NoneOptionalDatetime() + + assert.False(t, opt.IsSome()) + assert.True(t, opt.IsZero()) + + _, ok := opt.Get() + assert.False(t, ok) +} + +func TestOptionalDatetime_MustGet(t *testing.T) { + val, err := MakeDatetime(time.Now().In(time.UTC)) + assert.NoError(t, err) + optSome := SomeOptionalDatetime(val) + optNone := NoneOptionalDatetime() + + assert.Equal(t, val, optSome.MustGet()) + assert.Panics(t, func() { optNone.MustGet() }) +} + +func TestOptionalDatetime_Unwrap(t *testing.T) { + val, err := MakeDatetime(time.Now().In(time.UTC)) + assert.NoError(t, err) + optSome := SomeOptionalDatetime(val) + optNone := NoneOptionalDatetime() + + assert.Equal(t, val, optSome.Unwrap()) + assert.Equal(t, Datetime{}, optNone.Unwrap()) +} + +func TestOptionalDatetime_UnwrapOr(t *testing.T) { + val, err := MakeDatetime(time.Now().In(time.UTC)) + assert.NoError(t, err) + def, err := MakeDatetime(time.Now().Add(1 * time.Hour).In(time.UTC)) + assert.NoError(t, err) + optSome := SomeOptionalDatetime(val) + optNone := NoneOptionalDatetime() + + assert.Equal(t, val, optSome.UnwrapOr(def)) + assert.Equal(t, def, optNone.UnwrapOr(def)) +} + +func TestOptionalDatetime_UnwrapOrElse(t *testing.T) { + val, err := MakeDatetime(time.Now().In(time.UTC)) + assert.NoError(t, err) + def, err := MakeDatetime(time.Now().Add(1 * time.Hour).In(time.UTC)) + assert.NoError(t, err) + optSome := SomeOptionalDatetime(val) + optNone := NoneOptionalDatetime() + + assert.Equal(t, val, optSome.UnwrapOrElse(func() Datetime { return def })) + assert.Equal(t, def, optNone.UnwrapOrElse(func() Datetime { return def })) +} + +func TestOptionalDatetime_EncodeDecodeMsgpack_Some(t *testing.T) { + val, err := MakeDatetime(time.Now().In(time.UTC)) + assert.NoError(t, err) + some := SomeOptionalDatetime(val) + + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err = enc.Encode(some) + assert.NoError(t, err) + + var decodedSome OptionalDatetime + err = dec.Decode(&decodedSome) + assert.NoError(t, err) + assert.True(t, decodedSome.IsSome()) + assert.Equal(t, val, decodedSome.Unwrap()) +} + +func TestOptionalDatetime_EncodeDecodeMsgpack_None(t *testing.T) { + none := NoneOptionalDatetime() + + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(none) + assert.NoError(t, err) + + var decodedNone OptionalDatetime + err = dec.Decode(&decodedNone) + assert.NoError(t, err) + assert.True(t, decodedNone.IsZero()) +} + +func TestOptionalDatetime_EncodeDecodeMsgpack_InvalidType(t *testing.T) { + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(123) + assert.NoError(t, err) + + var decodedInvalid OptionalDatetime + err = dec.Decode(&decodedInvalid) + assert.Error(t, err) +} diff --git a/datetime/interval.go b/datetime/interval.go index bcd05238..e6d39e4d 100644 --- a/datetime/interval.go +++ b/datetime/interval.go @@ -2,7 +2,6 @@ package datetime import ( "bytes" - "fmt" "reflect" "github.com/vmihailenco/msgpack/v5" @@ -23,6 +22,8 @@ const ( ) // Interval type is GoLang implementation of Tarantool intervals. +// +//go:generate go tool gentypes -ext-code 6 Interval type Interval struct { Year int64 Month int64 @@ -35,6 +36,21 @@ type Interval struct { Adjust Adjust } +func (ival Interval) countNonZeroFields() int { + count := 0 + + for _, field := range []int64{ + ival.Year, ival.Month, ival.Week, ival.Day, ival.Hour, + ival.Min, ival.Sec, ival.Nsec, adjustToDt[ival.Adjust], + } { + if field != 0 { + count++ + } + } + + return count +} + // We use int64 for every field to avoid changes in the future, see: // https://github.com/tarantool/tarantool/blob/943ce3caf8401510ced4f074bca7006c3d73f9b3/src/lib/core/datetime.h#L106 @@ -66,115 +82,134 @@ func (ival Interval) Sub(sub Interval) Interval { return ival } -func encodeIntervalValue(e *msgpack.Encoder, typ uint64, value int64) (err error) { - if value == 0 { - return - } - err = e.EncodeUint(typ) - if err == nil { - if value > 0 { - err = e.EncodeUint(uint64(value)) - } else if value < 0 { - err = e.EncodeInt(value) - } +// MarshalMsgpack implements a custom msgpack marshaler. +func (ival Interval) MarshalMsgpack() ([]byte, error) { + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + + if err := ival.MarshalMsgpackTo(enc); err != nil { + return nil, err } - return -} -func encodeInterval(e *msgpack.Encoder, v reflect.Value) (err error) { - val := v.Interface().(Interval) + return buf.Bytes(), nil +} - var fieldNum uint64 - for _, val := range []int64{val.Year, val.Month, val.Week, val.Day, - val.Hour, val.Min, val.Sec, val.Nsec, - adjustToDt[val.Adjust]} { - if val != 0 { - fieldNum++ - } - } - if err = e.EncodeUint(fieldNum); err != nil { - return +// MarshalMsgpackTo implements a custom msgpack marshaler. +func (ival Interval) MarshalMsgpackTo(e *msgpack.Encoder) error { + var fieldNum = uint64(ival.countNonZeroFields()) + if err := e.EncodeUint(fieldNum); err != nil { + return err } - if err = encodeIntervalValue(e, fieldYear, val.Year); err != nil { - return + if err := encodeIntervalValue(e, fieldYear, ival.Year); err != nil { + return err } - if err = encodeIntervalValue(e, fieldMonth, val.Month); err != nil { - return + if err := encodeIntervalValue(e, fieldMonth, ival.Month); err != nil { + return err } - if err = encodeIntervalValue(e, fieldWeek, val.Week); err != nil { - return + if err := encodeIntervalValue(e, fieldWeek, ival.Week); err != nil { + return err } - if err = encodeIntervalValue(e, fieldDay, val.Day); err != nil { - return + if err := encodeIntervalValue(e, fieldDay, ival.Day); err != nil { + return err } - if err = encodeIntervalValue(e, fieldHour, val.Hour); err != nil { - return + if err := encodeIntervalValue(e, fieldHour, ival.Hour); err != nil { + return err } - if err = encodeIntervalValue(e, fieldMin, val.Min); err != nil { - return + if err := encodeIntervalValue(e, fieldMin, ival.Min); err != nil { + return err } - if err = encodeIntervalValue(e, fieldSec, val.Sec); err != nil { - return + if err := encodeIntervalValue(e, fieldSec, ival.Sec); err != nil { + return err } - if err = encodeIntervalValue(e, fieldNSec, val.Nsec); err != nil { - return + if err := encodeIntervalValue(e, fieldNSec, ival.Nsec); err != nil { + return err } - if err = encodeIntervalValue(e, fieldAdjust, adjustToDt[val.Adjust]); err != nil { - return + if err := encodeIntervalValue(e, fieldAdjust, adjustToDt[ival.Adjust]); err != nil { + return err } + return nil } -func decodeInterval(d *msgpack.Decoder, v reflect.Value) (err error) { - var fieldNum uint - if fieldNum, err = d.DecodeUint(); err != nil { - return +// UnmarshalMsgpackFrom implements a custom msgpack unmarshaler. +func (ival *Interval) UnmarshalMsgpackFrom(d *msgpack.Decoder) error { + fieldNum, err := d.DecodeUint() + if err != nil { + return err } - var val Interval + ival.Adjust = dtToAdjust[int64(NoneAdjust)] - hasAdjust := false for i := 0; i < int(fieldNum); i++ { var fieldType uint if fieldType, err = d.DecodeUint(); err != nil { - return + return err } + var fieldVal int64 if fieldVal, err = d.DecodeInt64(); err != nil { - return + return err } + switch fieldType { case fieldYear: - val.Year = fieldVal + ival.Year = fieldVal case fieldMonth: - val.Month = fieldVal + ival.Month = fieldVal case fieldWeek: - val.Week = fieldVal + ival.Week = fieldVal case fieldDay: - val.Day = fieldVal + ival.Day = fieldVal case fieldHour: - val.Hour = fieldVal + ival.Hour = fieldVal case fieldMin: - val.Min = fieldVal + ival.Min = fieldVal case fieldSec: - val.Sec = fieldVal + ival.Sec = fieldVal case fieldNSec: - val.Nsec = fieldVal + ival.Nsec = fieldVal case fieldAdjust: - hasAdjust = true - if adjust, ok := dtToAdjust[fieldVal]; ok { - val.Adjust = adjust - } else { - return fmt.Errorf("unsupported Adjust: %d", fieldVal) - } - default: - return fmt.Errorf("unsupported interval field type: %d", fieldType) + ival.Adjust = dtToAdjust[fieldVal] } } - if !hasAdjust { - val.Adjust = dtToAdjust[0] + return nil +} + +// UnmarshalMsgpack implements a custom msgpack unmarshaler. +func (ival *Interval) UnmarshalMsgpack(data []byte) error { + dec := msgpack.NewDecoder(bytes.NewReader(data)) + return ival.UnmarshalMsgpackFrom(dec) +} + +func encodeIntervalValue(e *msgpack.Encoder, typ uint64, value int64) error { + if value == 0 { + return nil + } + + err := e.EncodeUint(typ) + if err != nil { + return err + } + + switch { + case value > 0: + return e.EncodeUint(uint64(value)) + default: + return e.EncodeInt(value) + } +} + +func encodeInterval(e *msgpack.Encoder, v reflect.Value) (err error) { + val := v.Interface().(Interval) + return val.MarshalMsgpackTo(e) +} + +func decodeInterval(d *msgpack.Decoder, v reflect.Value) (err error) { + val := Interval{} + if err = val.UnmarshalMsgpackFrom(d); err != nil { + return } v.Set(reflect.ValueOf(val)) diff --git a/datetime/interval_gen.go b/datetime/interval_gen.go new file mode 100644 index 00000000..2cccaaca --- /dev/null +++ b/datetime/interval_gen.go @@ -0,0 +1,241 @@ +// Code generated by github.com/tarantool/go-option; DO NOT EDIT. + +package datetime + +import ( + "fmt" + + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" + + "github.com/tarantool/go-option" +) + +// OptionalInterval represents an optional value of type Interval. +// It can either hold a valid Interval (IsSome == true) or be empty (IsZero == true). +type OptionalInterval struct { + value Interval + exists bool +} + +// SomeOptionalInterval creates an optional OptionalInterval with the given Interval value. +// The returned OptionalInterval will have IsSome() == true and IsZero() == false. +func SomeOptionalInterval(value Interval) OptionalInterval { + return OptionalInterval{ + value: value, + exists: true, + } +} + +// NoneOptionalInterval creates an empty optional OptionalInterval value. +// The returned OptionalInterval will have IsSome() == false and IsZero() == true. +// +// Example: +// +// o := NoneOptionalInterval() +// if o.IsZero() { +// fmt.Println("value is absent") +// } +func NoneOptionalInterval() OptionalInterval { + return OptionalInterval{} +} + +func (o OptionalInterval) newEncodeError(err error) error { + if err == nil { + return nil + } + return &option.EncodeError{ + Type: "OptionalInterval", + Parent: err, + } +} + +func (o OptionalInterval) newDecodeError(err error) error { + if err == nil { + return nil + } + + return &option.DecodeError{ + Type: "OptionalInterval", + Parent: err, + } +} + +// IsSome returns true if the OptionalInterval contains a value. +// This indicates the value is explicitly set (not None). +func (o OptionalInterval) IsSome() bool { + return o.exists +} + +// IsZero returns true if the OptionalInterval 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 OptionalInterval) IsZero() bool { + return !o.exists +} + +// IsNil is an alias for IsZero. +// +// This method is provided for compatibility with the msgpack Encoder interface. +func (o OptionalInterval) 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 Interval, false). +// +// Recommended usage: +// +// if value, ok := o.Get(); ok { +// // use value +// } +func (o OptionalInterval) Get() (Interval, 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 OptionalInterval) MustGet() Interval { + 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 Interval. +// +// Warning: Does not check presence. Use IsSome() before calling if you need +// to distinguish between absent value and explicit zero value. +func (o OptionalInterval) Unwrap() Interval { + return o.value +} + +// UnwrapOr returns the stored value if present. +// Otherwise, returns the provided default value. +// +// Example: +// +// o := NoneOptionalInterval() +// v := o.UnwrapOr(someDefaultOptionalInterval) +func (o OptionalInterval) UnwrapOr(defaultValue Interval) Interval { + 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 := NoneOptionalInterval() +// v := o.UnwrapOrElse(func() Interval { return computeDefault() }) +func (o OptionalInterval) UnwrapOrElse(defaultValue func() Interval) Interval { + if o.exists { + return o.value + } + + return defaultValue() +} + +func (o OptionalInterval) encodeValue(encoder *msgpack.Encoder) error { + value, err := o.value.MarshalMsgpack() + if err != nil { + return err + } + + err = encoder.EncodeExtHeader(6, len(value)) + if err != nil { + return err + } + + _, err = encoder.Writer().Write(value) + if err != nil { + return err + } + + return nil +} + +// EncodeMsgpack encodes the OptionalInterval value using MessagePack format. +// - If the value is present, it is encoded as Interval. +// - If the value is absent (None), it is encoded as nil. +// +// Returns an error if encoding fails. +func (o OptionalInterval) EncodeMsgpack(encoder *msgpack.Encoder) error { + if o.exists { + return o.newEncodeError(o.encodeValue(encoder)) + } + + return o.newEncodeError(encoder.EncodeNil()) +} + +func (o *OptionalInterval) decodeValue(decoder *msgpack.Decoder) error { + tp, length, err := decoder.DecodeExtHeader() + switch { + case err != nil: + return o.newDecodeError(err) + case tp != 6: + return o.newDecodeError(fmt.Errorf("invalid extension code: %d", tp)) + } + + a := make([]byte, length) + if err := decoder.ReadFull(a); err != nil { + return o.newDecodeError(err) + } + + if err := o.value.UnmarshalMsgpack(a); err != nil { + return o.newDecodeError(err) + } + + o.exists = true + return nil +} + +func (o *OptionalInterval) checkCode(code byte) bool { + return msgpcode.IsExt(code) +} + +// DecodeMsgpack decodes a OptionalInterval value from MessagePack format. +// Supports two input types: +// - nil: interpreted as no value (NoneOptionalInterval) +// - Interval: interpreted as a present value (SomeOptionalInterval) +// +// Returns an error if the input type is unsupported or decoding fails. +// +// After successful decoding: +// - on nil: exists = false, value = default zero value +// - on Interval: exists = true, value = decoded value +func (o *OptionalInterval) DecodeMsgpack(decoder *msgpack.Decoder) error { + code, err := decoder.PeekCode() + if err != nil { + return o.newDecodeError(err) + } + + switch { + case code == msgpcode.Nil: + o.exists = false + + return o.newDecodeError(decoder.Skip()) + case o.checkCode(code): + err := o.decodeValue(decoder) + if err != nil { + return o.newDecodeError(err) + } + o.exists = true + + return err + default: + return o.newDecodeError(fmt.Errorf("unexpected code: %d", code)) + } +} diff --git a/datetime/interval_gen_test.go b/datetime/interval_gen_test.go new file mode 100644 index 00000000..162db333 --- /dev/null +++ b/datetime/interval_gen_test.go @@ -0,0 +1,116 @@ +package datetime + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/vmihailenco/msgpack/v5" +) + +func TestSomeOptionalInterval(t *testing.T) { + val := Interval{Year: 1} + opt := SomeOptionalInterval(val) + + assert.True(t, opt.IsSome()) + assert.False(t, opt.IsZero()) + + v, ok := opt.Get() + assert.True(t, ok) + assert.Equal(t, val, v) +} + +func TestNoneOptionalInterval(t *testing.T) { + opt := NoneOptionalInterval() + + assert.False(t, opt.IsSome()) + assert.True(t, opt.IsZero()) + + _, ok := opt.Get() + assert.False(t, ok) +} + +func TestOptionalInterval_MustGet(t *testing.T) { + val := Interval{Year: 1} + optSome := SomeOptionalInterval(val) + optNone := NoneOptionalInterval() + + assert.Equal(t, val, optSome.MustGet()) + assert.Panics(t, func() { optNone.MustGet() }) +} + +func TestOptionalInterval_Unwrap(t *testing.T) { + val := Interval{Year: 1} + optSome := SomeOptionalInterval(val) + optNone := NoneOptionalInterval() + + assert.Equal(t, val, optSome.Unwrap()) + assert.Equal(t, Interval{}, optNone.Unwrap()) +} + +func TestOptionalInterval_UnwrapOr(t *testing.T) { + val := Interval{Year: 1} + def := Interval{Year: 2} + optSome := SomeOptionalInterval(val) + optNone := NoneOptionalInterval() + + assert.Equal(t, val, optSome.UnwrapOr(def)) + assert.Equal(t, def, optNone.UnwrapOr(def)) +} + +func TestOptionalInterval_UnwrapOrElse(t *testing.T) { + val := Interval{Year: 1} + def := Interval{Year: 2} + optSome := SomeOptionalInterval(val) + optNone := NoneOptionalInterval() + + assert.Equal(t, val, optSome.UnwrapOrElse(func() Interval { return def })) + assert.Equal(t, def, optNone.UnwrapOrElse(func() Interval { return def })) +} + +func TestOptionalInterval_EncodeDecodeMsgpack_Some(t *testing.T) { + val := Interval{Year: 1} + some := SomeOptionalInterval(val) + + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(some) + assert.NoError(t, err) + + var decodedSome OptionalInterval + err = dec.Decode(&decodedSome) + assert.NoError(t, err) + assert.True(t, decodedSome.IsSome()) + assert.Equal(t, val, decodedSome.Unwrap()) +} + +func TestOptionalInterval_EncodeDecodeMsgpack_None(t *testing.T) { + none := NoneOptionalInterval() + + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(none) + assert.NoError(t, err) + + var decodedNone OptionalInterval + err = dec.Decode(&decodedNone) + assert.NoError(t, err) + assert.True(t, decodedNone.IsZero()) +} + +func TestOptionalInterval_EncodeDecodeMsgpack_InvalidType(t *testing.T) { + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(123) + assert.NoError(t, err) + + var decodedInvalid OptionalInterval + err = dec.Decode(&decodedInvalid) + assert.Error(t, err) +} diff --git a/decimal/decimal.go b/decimal/decimal.go index 3a1abb76..3c268123 100644 --- a/decimal/decimal.go +++ b/decimal/decimal.go @@ -44,13 +44,14 @@ const ( ) var ( - one decimal.Decimal = decimal.NewFromInt(1) + one = decimal.NewFromInt(1) // -10^decimalPrecision - 1 - minSupportedDecimal decimal.Decimal = maxSupportedDecimal.Neg().Sub(one) + minSupportedDecimal = maxSupportedDecimal.Neg().Sub(one) // 10^decimalPrecision - 1 - maxSupportedDecimal decimal.Decimal = decimal.New(1, decimalPrecision).Sub(one) + maxSupportedDecimal = decimal.New(1, decimalPrecision).Sub(one) ) +//go:generate go tool gentypes -ext-code 1 Decimal type Decimal struct { decimal.Decimal } @@ -71,37 +72,20 @@ func MakeDecimalFromString(src string) (Decimal, error) { return result, nil } -func decimalEncoder(e *msgpack.Encoder, v reflect.Value) ([]byte, error) { - dec := v.Interface().(Decimal) - if dec.GreaterThan(maxSupportedDecimal) { - return nil, - fmt.Errorf( - "msgpack: decimal number is bigger than maximum supported number (10^%d - 1)", - decimalPrecision) - } - if dec.LessThan(minSupportedDecimal) { - return nil, - fmt.Errorf( - "msgpack: decimal number is lesser than minimum supported number (-10^%d - 1)", - decimalPrecision) - } - - strBuf := dec.String() - bcdBuf, err := encodeStringToBCD(strBuf) - if err != nil { - return nil, fmt.Errorf("msgpack: can't encode string (%s) to a BCD buffer: %w", strBuf, err) - } - return bcdBuf, nil -} +var ( + ErrDecimalOverflow = fmt.Errorf("msgpack: decimal number is bigger than"+ + " maximum supported number (10^%d - 1)", decimalPrecision) + ErrDecimalUnderflow = fmt.Errorf("msgpack: decimal number is lesser than"+ + " minimum supported number (-10^%d - 1)", decimalPrecision) +) -func decimalDecoder(d *msgpack.Decoder, v reflect.Value, extLen int) error { - b := make([]byte, extLen) - n, err := d.Buffered().Read(b) - if err != nil { - return err - } - if n < extLen { - return fmt.Errorf("msgpack: unexpected end of stream after %d decimal bytes", n) +// MarshalMsgpack implements a custom msgpack marshaler. +func (d Decimal) MarshalMsgpack() ([]byte, error) { + switch { + case d.GreaterThan(maxSupportedDecimal): + return nil, ErrDecimalOverflow + case d.LessThan(minSupportedDecimal): + return nil, ErrDecimalUnderflow } // Decimal values can be encoded to fixext MessagePack, where buffer @@ -112,9 +96,19 @@ func decimalDecoder(d *msgpack.Decoder, v reflect.Value, extLen int) error { // +--------+-------------------+------------+===============+ // | MP_EXT | length (optional) | MP_DECIMAL | PackedDecimal | // +--------+-------------------+------------+===============+ - digits, exp, err := decodeStringFromBCD(b) + strBuf := d.String() + bcdBuf, err := encodeStringToBCD(strBuf) if err != nil { - return fmt.Errorf("msgpack: can't decode string from BCD buffer (%x): %w", b, err) + return nil, fmt.Errorf("msgpack: can't encode string (%s) to a BCD buffer: %w", strBuf, err) + } + return bcdBuf, nil +} + +// UnmarshalMsgpack implements a custom msgpack unmarshaler. +func (d *Decimal) UnmarshalMsgpack(data []byte) error { + digits, exp, err := decodeStringFromBCD(data) + if err != nil { + return fmt.Errorf("msgpack: can't decode string from BCD buffer (%x): %w", data, err) } dec, err := decimal.NewFromString(digits) @@ -125,11 +119,31 @@ func decimalDecoder(d *msgpack.Decoder, v reflect.Value, extLen int) error { if exp != 0 { dec = dec.Shift(int32(exp)) } - ptr := v.Addr().Interface().(*Decimal) - *ptr = MakeDecimal(dec) + + *d = MakeDecimal(dec) return nil } +func decimalEncoder(e *msgpack.Encoder, v reflect.Value) ([]byte, error) { + dec := v.Interface().(Decimal) + + return dec.MarshalMsgpack() +} + +func decimalDecoder(d *msgpack.Decoder, v reflect.Value, extLen int) error { + b := make([]byte, extLen) + + switch n, err := d.Buffered().Read(b); { + case err != nil: + return err + case n < extLen: + return fmt.Errorf("msgpack: unexpected end of stream after %d decimal bytes", n) + } + + ptr := v.Addr().Interface().(*Decimal) + return ptr.UnmarshalMsgpack(b) +} + func init() { msgpack.RegisterExtDecoder(decimalExtID, Decimal{}, decimalDecoder) msgpack.RegisterExtEncoder(decimalExtID, Decimal{}, decimalEncoder) diff --git a/decimal/decimal_gen.go b/decimal/decimal_gen.go new file mode 100644 index 00000000..0f9b18e3 --- /dev/null +++ b/decimal/decimal_gen.go @@ -0,0 +1,241 @@ +// Code generated by github.com/tarantool/go-option; DO NOT EDIT. + +package decimal + +import ( + "fmt" + + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" + + "github.com/tarantool/go-option" +) + +// OptionalDecimal represents an optional value of type Decimal. +// It can either hold a valid Decimal (IsSome == true) or be empty (IsZero == true). +type OptionalDecimal struct { + value Decimal + exists bool +} + +// SomeOptionalDecimal creates an optional OptionalDecimal with the given Decimal value. +// The returned OptionalDecimal will have IsSome() == true and IsZero() == false. +func SomeOptionalDecimal(value Decimal) OptionalDecimal { + return OptionalDecimal{ + value: value, + exists: true, + } +} + +// NoneOptionalDecimal creates an empty optional OptionalDecimal value. +// The returned OptionalDecimal will have IsSome() == false and IsZero() == true. +// +// Example: +// +// o := NoneOptionalDecimal() +// if o.IsZero() { +// fmt.Println("value is absent") +// } +func NoneOptionalDecimal() OptionalDecimal { + return OptionalDecimal{} +} + +func (o OptionalDecimal) newEncodeError(err error) error { + if err == nil { + return nil + } + return &option.EncodeError{ + Type: "OptionalDecimal", + Parent: err, + } +} + +func (o OptionalDecimal) newDecodeError(err error) error { + if err == nil { + return nil + } + + return &option.DecodeError{ + Type: "OptionalDecimal", + Parent: err, + } +} + +// IsSome returns true if the OptionalDecimal contains a value. +// This indicates the value is explicitly set (not None). +func (o OptionalDecimal) IsSome() bool { + return o.exists +} + +// IsZero returns true if the OptionalDecimal 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 OptionalDecimal) IsZero() bool { + return !o.exists +} + +// IsNil is an alias for IsZero. +// +// This method is provided for compatibility with the msgpack Encoder interface. +func (o OptionalDecimal) 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 Decimal, false). +// +// Recommended usage: +// +// if value, ok := o.Get(); ok { +// // use value +// } +func (o OptionalDecimal) Get() (Decimal, 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 OptionalDecimal) MustGet() Decimal { + 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 Decimal. +// +// Warning: Does not check presence. Use IsSome() before calling if you need +// to distinguish between absent value and explicit zero value. +func (o OptionalDecimal) Unwrap() Decimal { + return o.value +} + +// UnwrapOr returns the stored value if present. +// Otherwise, returns the provided default value. +// +// Example: +// +// o := NoneOptionalDecimal() +// v := o.UnwrapOr(someDefaultOptionalDecimal) +func (o OptionalDecimal) UnwrapOr(defaultValue Decimal) Decimal { + 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 := NoneOptionalDecimal() +// v := o.UnwrapOrElse(func() Decimal { return computeDefault() }) +func (o OptionalDecimal) UnwrapOrElse(defaultValue func() Decimal) Decimal { + if o.exists { + return o.value + } + + return defaultValue() +} + +func (o OptionalDecimal) encodeValue(encoder *msgpack.Encoder) error { + value, err := o.value.MarshalMsgpack() + if err != nil { + return err + } + + err = encoder.EncodeExtHeader(1, len(value)) + if err != nil { + return err + } + + _, err = encoder.Writer().Write(value) + if err != nil { + return err + } + + return nil +} + +// EncodeMsgpack encodes the OptionalDecimal value using MessagePack format. +// - If the value is present, it is encoded as Decimal. +// - If the value is absent (None), it is encoded as nil. +// +// Returns an error if encoding fails. +func (o OptionalDecimal) EncodeMsgpack(encoder *msgpack.Encoder) error { + if o.exists { + return o.newEncodeError(o.encodeValue(encoder)) + } + + return o.newEncodeError(encoder.EncodeNil()) +} + +func (o *OptionalDecimal) decodeValue(decoder *msgpack.Decoder) error { + tp, length, err := decoder.DecodeExtHeader() + switch { + case err != nil: + return o.newDecodeError(err) + case tp != 1: + return o.newDecodeError(fmt.Errorf("invalid extension code: %d", tp)) + } + + a := make([]byte, length) + if err := decoder.ReadFull(a); err != nil { + return o.newDecodeError(err) + } + + if err := o.value.UnmarshalMsgpack(a); err != nil { + return o.newDecodeError(err) + } + + o.exists = true + return nil +} + +func (o *OptionalDecimal) checkCode(code byte) bool { + return msgpcode.IsExt(code) +} + +// DecodeMsgpack decodes a OptionalDecimal value from MessagePack format. +// Supports two input types: +// - nil: interpreted as no value (NoneOptionalDecimal) +// - Decimal: interpreted as a present value (SomeOptionalDecimal) +// +// Returns an error if the input type is unsupported or decoding fails. +// +// After successful decoding: +// - on nil: exists = false, value = default zero value +// - on Decimal: exists = true, value = decoded value +func (o *OptionalDecimal) DecodeMsgpack(decoder *msgpack.Decoder) error { + code, err := decoder.PeekCode() + if err != nil { + return o.newDecodeError(err) + } + + switch { + case code == msgpcode.Nil: + o.exists = false + + return o.newDecodeError(decoder.Skip()) + case o.checkCode(code): + err := o.decodeValue(decoder) + if err != nil { + return o.newDecodeError(err) + } + o.exists = true + + return err + default: + return o.newDecodeError(fmt.Errorf("unexpected code: %d", code)) + } +} diff --git a/decimal/decimal_gen_test.go b/decimal/decimal_gen_test.go new file mode 100644 index 00000000..50f22bf2 --- /dev/null +++ b/decimal/decimal_gen_test.go @@ -0,0 +1,117 @@ +package decimal + +import ( + "bytes" + "testing" + + "github.com/shopspring/decimal" + "github.com/stretchr/testify/assert" + "github.com/vmihailenco/msgpack/v5" +) + +func TestSomeOptionalDecimal(t *testing.T) { + val := MakeDecimal(decimal.NewFromFloat(1.23)) + opt := SomeOptionalDecimal(val) + + assert.True(t, opt.IsSome()) + assert.False(t, opt.IsZero()) + + v, ok := opt.Get() + assert.True(t, ok) + assert.Equal(t, val, v) +} + +func TestNoneOptionalDecimal(t *testing.T) { + opt := NoneOptionalDecimal() + + assert.False(t, opt.IsSome()) + assert.True(t, opt.IsZero()) + + _, ok := opt.Get() + assert.False(t, ok) +} + +func TestOptionalDecimal_MustGet(t *testing.T) { + val := MakeDecimal(decimal.NewFromFloat(1.23)) + optSome := SomeOptionalDecimal(val) + optNone := NoneOptionalDecimal() + + assert.Equal(t, val, optSome.MustGet()) + assert.Panics(t, func() { optNone.MustGet() }) +} + +func TestOptionalDecimal_Unwrap(t *testing.T) { + val := MakeDecimal(decimal.NewFromFloat(1.23)) + optSome := SomeOptionalDecimal(val) + optNone := NoneOptionalDecimal() + + assert.Equal(t, val, optSome.Unwrap()) + assert.Equal(t, Decimal{}, optNone.Unwrap()) +} + +func TestOptionalDecimal_UnwrapOr(t *testing.T) { + val := MakeDecimal(decimal.NewFromFloat(1.23)) + def := MakeDecimal(decimal.NewFromFloat(4.56)) + optSome := SomeOptionalDecimal(val) + optNone := NoneOptionalDecimal() + + assert.Equal(t, val, optSome.UnwrapOr(def)) + assert.Equal(t, def, optNone.UnwrapOr(def)) +} + +func TestOptionalDecimal_UnwrapOrElse(t *testing.T) { + val := MakeDecimal(decimal.NewFromFloat(1.23)) + def := MakeDecimal(decimal.NewFromFloat(4.56)) + optSome := SomeOptionalDecimal(val) + optNone := NoneOptionalDecimal() + + assert.Equal(t, val, optSome.UnwrapOrElse(func() Decimal { return def })) + assert.Equal(t, def, optNone.UnwrapOrElse(func() Decimal { return def })) +} + +func TestOptionalDecimal_EncodeDecodeMsgpack_Some(t *testing.T) { + val := MakeDecimal(decimal.NewFromFloat(1.23)) + some := SomeOptionalDecimal(val) + + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(some) + assert.NoError(t, err) + + var decodedSome OptionalDecimal + err = dec.Decode(&decodedSome) + assert.NoError(t, err) + assert.True(t, decodedSome.IsSome()) + assert.Equal(t, val, decodedSome.Unwrap()) +} + +func TestOptionalDecimal_EncodeDecodeMsgpack_None(t *testing.T) { + none := NoneOptionalDecimal() + + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(none) + assert.NoError(t, err) + + var decodedNone OptionalDecimal + err = dec.Decode(&decodedNone) + assert.NoError(t, err) + assert.True(t, decodedNone.IsZero()) +} + +func TestOptionalDecimal_EncodeDecodeMsgpack_InvalidType(t *testing.T) { + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(123) + assert.NoError(t, err) + + var decodedInvalid OptionalDecimal + err = dec.Decode(&decodedInvalid) + assert.Error(t, err) +} diff --git a/go.mod b/go.mod index c1d57fa1..7582412d 100644 --- a/go.mod +++ b/go.mod @@ -3,16 +3,27 @@ module github.com/tarantool/go-tarantool/v3 go 1.24 require ( - github.com/google/uuid v1.3.0 + github.com/google/uuid v1.6.0 github.com/shopspring/decimal v1.3.1 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.11.1 github.com/tarantool/go-iproto v1.1.0 + github.com/tarantool/go-option v1.0.0 github.com/vmihailenco/msgpack/v5 v5.4.1 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/kr/text v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + golang.org/x/mod v0.27.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/tools v0.36.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +tool ( + github.com/tarantool/go-option/cmd/gentypes + golang.org/x/tools/cmd/stringer +) diff --git a/go.sum b/go.sum index 099647b8..91e1c4f2 100644 --- a/go.sum +++ b/go.sum @@ -1,20 +1,38 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tarantool/go-iproto v1.1.0 h1:HULVOIHsiehI+FnHfM7wMDntuzUddO09DKqu2WnFQ5A= github.com/tarantool/go-iproto v1.1.0/go.mod h1:LNCtdyZxojUed8SbOiYHoc3v9NvaZTB7p96hUySMlIo= +github.com/tarantool/go-option v1.0.0 h1:+Etw0i3TjsXvADTo5rfZNCfsXe3BfHOs+iVfIrl0Nlo= +github.com/tarantool/go-option v1.0.0/go.mod h1:lXzzeZtL+rPUtLOCDP6ny3FemFBjruG9aHKzNN2bS08= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pool/const.go b/pool/const.go index b1748ae5..d1549092 100644 --- a/pool/const.go +++ b/pool/const.go @@ -1,4 +1,4 @@ -//go:generate stringer -type Role -linecomment +//go:generate go tool stringer -type Role -linecomment package pool /* diff --git a/uuid/uuid.go b/uuid/uuid.go index cc2be736..ca7b0ad0 100644 --- a/uuid/uuid.go +++ b/uuid/uuid.go @@ -1,4 +1,4 @@ -// Package with support of Tarantool's UUID data type. +// Package uuid with support of Tarantool's UUID data type. // // UUID data type supported in Tarantool since 2.4.1. // @@ -27,6 +27,17 @@ import ( // UUID external type. const uuid_extID = 2 +//go:generate go tool gentypes -ext-code 2 -marshal-func marshalUUID -unmarshal-func unmarshalUUID -imports "github.com/google/uuid" uuid.UUID + +func marshalUUID(id uuid.UUID) ([]byte, error) { + return id.MarshalBinary() +} + +func unmarshalUUID(uuid *uuid.UUID, data []byte) error { + return uuid.UnmarshalBinary(data) +} + +// encodeUUID encodes a uuid.UUID value into the msgpack format. func encodeUUID(e *msgpack.Encoder, v reflect.Value) error { id := v.Interface().(uuid.UUID) @@ -43,6 +54,7 @@ func encodeUUID(e *msgpack.Encoder, v reflect.Value) error { return nil } +// decodeUUID decodes a uuid.UUID value from the msgpack format. func decodeUUID(d *msgpack.Decoder, v reflect.Value) error { var bytesCount = 16 bytes := make([]byte, bytesCount) diff --git a/uuid/uuid_gen.go b/uuid/uuid_gen.go new file mode 100644 index 00000000..f1b1992c --- /dev/null +++ b/uuid/uuid_gen.go @@ -0,0 +1,243 @@ +// Code generated by github.com/tarantool/go-option; DO NOT EDIT. + +package uuid + +import ( + "github.com/google/uuid" + + "fmt" + + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" + + "github.com/tarantool/go-option" +) + +// OptionalUUID represents an optional value of type uuid.UUID. +// It can either hold a valid uuid.UUID (IsSome == true) or be empty (IsZero == true). +type OptionalUUID struct { + value uuid.UUID + exists bool +} + +// SomeOptionalUUID creates an optional OptionalUUID with the given uuid.UUID value. +// The returned OptionalUUID will have IsSome() == true and IsZero() == false. +func SomeOptionalUUID(value uuid.UUID) OptionalUUID { + return OptionalUUID{ + value: value, + exists: true, + } +} + +// NoneOptionalUUID creates an empty optional OptionalUUID value. +// The returned OptionalUUID will have IsSome() == false and IsZero() == true. +// +// Example: +// +// o := NoneOptionalUUID() +// if o.IsZero() { +// fmt.Println("value is absent") +// } +func NoneOptionalUUID() OptionalUUID { + return OptionalUUID{} +} + +func (o OptionalUUID) newEncodeError(err error) error { + if err == nil { + return nil + } + return &option.EncodeError{ + Type: "OptionalUUID", + Parent: err, + } +} + +func (o OptionalUUID) newDecodeError(err error) error { + if err == nil { + return nil + } + + return &option.DecodeError{ + Type: "OptionalUUID", + Parent: err, + } +} + +// IsSome returns true if the OptionalUUID contains a value. +// This indicates the value is explicitly set (not None). +func (o OptionalUUID) IsSome() bool { + return o.exists +} + +// IsZero returns true if the OptionalUUID 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 OptionalUUID) IsZero() bool { + return !o.exists +} + +// IsNil is an alias for IsZero. +// +// This method is provided for compatibility with the msgpack Encoder interface. +func (o OptionalUUID) 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 uuid.UUID, false). +// +// Recommended usage: +// +// if value, ok := o.Get(); ok { +// // use value +// } +func (o OptionalUUID) Get() (uuid.UUID, 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 OptionalUUID) MustGet() uuid.UUID { + 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 uuid.UUID. +// +// Warning: Does not check presence. Use IsSome() before calling if you need +// to distinguish between absent value and explicit zero value. +func (o OptionalUUID) Unwrap() uuid.UUID { + return o.value +} + +// UnwrapOr returns the stored value if present. +// Otherwise, returns the provided default value. +// +// Example: +// +// o := NoneOptionalUUID() +// v := o.UnwrapOr(someDefaultOptionalUUID) +func (o OptionalUUID) UnwrapOr(defaultValue uuid.UUID) uuid.UUID { + 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 := NoneOptionalUUID() +// v := o.UnwrapOrElse(func() uuid.UUID { return computeDefault() }) +func (o OptionalUUID) UnwrapOrElse(defaultValue func() uuid.UUID) uuid.UUID { + if o.exists { + return o.value + } + + return defaultValue() +} + +func (o OptionalUUID) encodeValue(encoder *msgpack.Encoder) error { + value, err := marshalUUID(o.value) + if err != nil { + return err + } + + err = encoder.EncodeExtHeader(2, len(value)) + if err != nil { + return err + } + + _, err = encoder.Writer().Write(value) + if err != nil { + return err + } + + return nil +} + +// EncodeMsgpack encodes the OptionalUUID value using MessagePack format. +// - If the value is present, it is encoded as uuid.UUID. +// - If the value is absent (None), it is encoded as nil. +// +// Returns an error if encoding fails. +func (o OptionalUUID) EncodeMsgpack(encoder *msgpack.Encoder) error { + if o.exists { + return o.newEncodeError(o.encodeValue(encoder)) + } + + return o.newEncodeError(encoder.EncodeNil()) +} + +func (o *OptionalUUID) decodeValue(decoder *msgpack.Decoder) error { + tp, length, err := decoder.DecodeExtHeader() + switch { + case err != nil: + return o.newDecodeError(err) + case tp != 2: + return o.newDecodeError(fmt.Errorf("invalid extension code: %d", tp)) + } + + a := make([]byte, length) + if err := decoder.ReadFull(a); err != nil { + return o.newDecodeError(err) + } + + if err := unmarshalUUID(&o.value, a); err != nil { + return o.newDecodeError(err) + } + + o.exists = true + return nil +} + +func (o *OptionalUUID) checkCode(code byte) bool { + return msgpcode.IsExt(code) +} + +// DecodeMsgpack decodes a OptionalUUID value from MessagePack format. +// Supports two input types: +// - nil: interpreted as no value (NoneOptionalUUID) +// - uuid.UUID: interpreted as a present value (SomeOptionalUUID) +// +// Returns an error if the input type is unsupported or decoding fails. +// +// After successful decoding: +// - on nil: exists = false, value = default zero value +// - on uuid.UUID: exists = true, value = decoded value +func (o *OptionalUUID) DecodeMsgpack(decoder *msgpack.Decoder) error { + code, err := decoder.PeekCode() + if err != nil { + return o.newDecodeError(err) + } + + switch { + case code == msgpcode.Nil: + o.exists = false + + return o.newDecodeError(decoder.Skip()) + case o.checkCode(code): + err := o.decodeValue(decoder) + if err != nil { + return o.newDecodeError(err) + } + o.exists = true + + return err + default: + return o.newDecodeError(fmt.Errorf("unexpected code: %d", code)) + } +} diff --git a/uuid/uuid_gen_test.go b/uuid/uuid_gen_test.go new file mode 100644 index 00000000..616bb231 --- /dev/null +++ b/uuid/uuid_gen_test.go @@ -0,0 +1,117 @@ +package uuid + +import ( + "bytes" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/vmihailenco/msgpack/v5" +) + +func TestSomeOptionalUUID(t *testing.T) { + val := uuid.New() + opt := SomeOptionalUUID(val) + + assert.True(t, opt.IsSome()) + assert.False(t, opt.IsZero()) + + v, ok := opt.Get() + assert.True(t, ok) + assert.Equal(t, val, v) +} + +func TestNoneOptionalUUID(t *testing.T) { + opt := NoneOptionalUUID() + + assert.False(t, opt.IsSome()) + assert.True(t, opt.IsZero()) + + _, ok := opt.Get() + assert.False(t, ok) +} + +func TestOptionalUUID_MustGet(t *testing.T) { + val := uuid.New() + optSome := SomeOptionalUUID(val) + optNone := NoneOptionalUUID() + + assert.Equal(t, val, optSome.MustGet()) + assert.Panics(t, func() { optNone.MustGet() }) +} + +func TestOptionalUUID_Unwrap(t *testing.T) { + val := uuid.New() + optSome := SomeOptionalUUID(val) + optNone := NoneOptionalUUID() + + assert.Equal(t, val, optSome.Unwrap()) + assert.Equal(t, uuid.Nil, optNone.Unwrap()) +} + +func TestOptionalUUID_UnwrapOr(t *testing.T) { + val := uuid.New() + def := uuid.New() + optSome := SomeOptionalUUID(val) + optNone := NoneOptionalUUID() + + assert.Equal(t, val, optSome.UnwrapOr(def)) + assert.Equal(t, def, optNone.UnwrapOr(def)) +} + +func TestOptionalUUID_UnwrapOrElse(t *testing.T) { + val := uuid.New() + def := uuid.New() + optSome := SomeOptionalUUID(val) + optNone := NoneOptionalUUID() + + assert.Equal(t, val, optSome.UnwrapOrElse(func() uuid.UUID { return def })) + assert.Equal(t, def, optNone.UnwrapOrElse(func() uuid.UUID { return def })) +} + +func TestOptionalUUID_EncodeDecodeMsgpack_Some(t *testing.T) { + val := uuid.New() + some := SomeOptionalUUID(val) + + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(some) + assert.NoError(t, err) + + var decodedSome OptionalUUID + err = dec.Decode(&decodedSome) + assert.NoError(t, err) + assert.True(t, decodedSome.IsSome()) + assert.Equal(t, val, decodedSome.Unwrap()) +} + +func TestOptionalUUID_EncodeDecodeMsgpack_None(t *testing.T) { + none := NoneOptionalUUID() + + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(none) + assert.NoError(t, err) + + var decodedNone OptionalUUID + err = dec.Decode(&decodedNone) + assert.NoError(t, err) + assert.True(t, decodedNone.IsZero()) +} + +func TestOptionalUUID_EncodeDecodeMsgpack_InvalidType(t *testing.T) { + var buf bytes.Buffer + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + err := enc.Encode(123) + assert.NoError(t, err) + + var decodedInvalid OptionalUUID + err = dec.Decode(&decodedInvalid) + assert.Error(t, err) +}