Skip to content

Commit

Permalink
feat: accept convertible types (#250)
Browse files Browse the repository at this point in the history
  • Loading branch information
TuSKan authored Apr 8, 2023
1 parent 2c92121 commit 9ec8016
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 20 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,12 @@ encoding and decoding.

Enums may also implement `TextMarshaler` and `TextUnmarshaler`, and must resolve to valid symbols in the given enum schema.

##### Identical Underlying Types
One type can be [ConvertibleTo](https://go.dev/ref/spec#Conversions) another type if they have identical underlying types.
A non-native type is allowed be used if it can be convertible to *time.Time*, *big.Rat* or *avro.LogicalDuration* for the particular of *LogicalTypes*.

Ex.: `type Timestamp time.Time`

### Recursive Structs

At this moment recursive structs are not supported. It is planned for the future.
Expand Down
6 changes: 3 additions & 3 deletions codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import (
)

var (
timeRType = reflect2.TypeOf(time.Time{}).RType()
ratRType = reflect2.TypeOf(big.Rat{}).RType()
durRType = reflect2.TypeOf(LogicalDuration{}).RType()
timeType = reflect.TypeOf(time.Time{})
ratType = reflect.TypeOf(big.Rat{})
durType = reflect.TypeOf(LogicalDuration{})
)

type null struct{}
Expand Down
117 changes: 117 additions & 0 deletions codec_convertible_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package avro_test

import (
"bytes"
"testing"
"time"

"github.com/hamba/avro/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestDecoder_ConvertiblePtr(t *testing.T) {
defer ConfigTeardown()

data := []byte{0x90, 0xb2, 0xae, 0xc3, 0xec, 0x5b}
schema := `{"type": "long", "logicalType": "timestamp-millis"}`
dec, err := avro.NewDecoder(schema, bytes.NewReader(data))
require.NoError(t, err)

var ts TestTimestampPtr
err = dec.Decode(&ts)

require.NoError(t, err)
want := TestTimestampPtr(time.Date(2020, 01, 02, 03, 04, 05, 00, time.UTC))
assert.Equal(t, want, ts)
}

func TestDecoder_ConvertiblePtrPtr(t *testing.T) {
defer ConfigTeardown()

data := []byte{0x2, 0x90, 0xb2, 0xae, 0xc3, 0xec, 0x5b}
schema := `{"type" : ["null", {"type": "long", "logicalType": "timestamp-millis"}]}`
dec, err := avro.NewDecoder(schema, bytes.NewReader(data))
require.NoError(t, err)

var ts *TestTimestampPtr
err = dec.Decode(&ts)

require.NoError(t, err)
assert.NotNil(t, ts)
want := TestTimestampPtr(time.Date(2020, 01, 02, 03, 04, 05, 00, time.UTC))
assert.Equal(t, want, *ts)
}

func TestDecoder_ConvertibleError(t *testing.T) {
defer ConfigTeardown()

data := []byte{0x90, 0xb2, 0xae, 0xc3, 0xec, 0x5b}
schema := `{"type": "long", "logicalType": "timestamp-millis"}`
dec, err := avro.NewDecoder(schema, bytes.NewReader(data))
require.NoError(t, err)

var ts *TestTimestampError
err = dec.Decode(&ts)

assert.Error(t, err)
}

func TestEncoder_Convertible(t *testing.T) {
defer ConfigTeardown()

schema := `{"type": "long", "logicalType": "timestamp-millis"}`
buf := bytes.NewBuffer([]byte{})
enc, err := avro.NewEncoder(schema, buf)
require.NoError(t, err)
ts := TestTimestamp(time.Date(2020, 01, 02, 03, 04, 05, 00, time.UTC))

err = enc.Encode(ts)

require.NoError(t, err)
assert.Equal(t, []byte{0x90, 0xb2, 0xae, 0xc3, 0xec, 0x5b}, buf.Bytes())
}

func TestEncoder_ConvertiblePtr(t *testing.T) {
defer ConfigTeardown()

schema := `{"type" : ["null", {"type": "long", "logicalType": "timestamp-millis"}]}`
buf := bytes.NewBuffer([]byte{})
enc, err := avro.NewEncoder(schema, buf)
require.NoError(t, err)
ts := TestTimestampPtr(time.Date(2020, 01, 02, 03, 04, 05, 00, time.UTC))

err = enc.Encode(&ts)

require.NoError(t, err)
assert.Equal(t, []byte{0x2, 0x90, 0xb2, 0xae, 0xc3, 0xec, 0x5b}, buf.Bytes())
}

func TestEncoder_ConvertiblePtrNil(t *testing.T) {
defer ConfigTeardown()

schema := `{"type" : ["null", {"type": "long", "logicalType": "timestamp-millis"}]}`
buf := bytes.NewBuffer([]byte{})
enc, err := avro.NewEncoder(schema, buf)
require.NoError(t, err)
var ts *TestTimestampPtr

err = enc.Encode(ts)

require.NoError(t, err)
assert.Equal(t, []byte{0x00}, buf.Bytes())
}

func TestEncoder_ConvertibleError(t *testing.T) {
defer ConfigTeardown()

schema := `{"type": "long", "logicalType": "timestamp-millis"}`
buf := bytes.NewBuffer([]byte{})
enc, err := avro.NewEncoder(schema, buf)
require.NoError(t, err)
ts := TestTimestampError{}

err = enc.Encode(&ts)

assert.Error(t, err)
}
12 changes: 8 additions & 4 deletions codec_fixed.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ func createDecoderOfFixed(schema Schema, typ reflect2.Type) ValDecoder {
if ls == nil {
break
}
typ1 := typ.Type1()
switch {
case typ.RType() == durRType && ls.Type() == Duration:
case typ1.ConvertibleTo(durType) && ls.Type() == Duration:
return &fixedDurationCodec{}
case typ.RType() == ratRType && ls.Type() == Decimal:
case typ1.ConvertibleTo(ratType) && ls.Type() == Decimal:
dec := ls.(*DecimalLogicalSchema)
return &fixedDecimalCodec{prec: dec.Precision(), scale: dec.Scale(), size: fixed.Size()}
}
Expand All @@ -55,7 +56,9 @@ func createEncoderOfFixed(schema Schema, typ reflect2.Type) ValEncoder {
elemType := ptrType.Elem()

ls := fixed.Logical()
if elemType.Kind() != reflect.Struct || elemType.RType() != ratRType || ls == nil || ls.Type() != Decimal {
tpy1 := elemType.Type1()
if elemType.Kind() != reflect.Struct || !tpy1.ConvertibleTo(ratType) || ls == nil ||
ls.Type() != Decimal {
break
}
dec := ls.(*DecimalLogicalSchema)
Expand All @@ -66,7 +69,8 @@ func createEncoderOfFixed(schema Schema, typ reflect2.Type) ValEncoder {
if ls == nil {
break
}
if typ.RType() == durRType && ls.Type() == Duration {
typ1 := typ.Type1()
if typ1.ConvertibleTo(durType) && ls.Type() == Duration {
return &fixedDurationCodec{}
}
}
Expand Down
31 changes: 18 additions & 13 deletions codec_native.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,17 +105,20 @@ func createDecoderOfNative(schema Schema, typ reflect2.Type) ValDecoder {
st := schema.Type()
ls := getLogicalSchema(schema)
lt := getLogicalType(schema)
tpy1 := typ.Type1()
Istpy1Time := tpy1.ConvertibleTo(timeType)
Istpy1Rat := tpy1.ConvertibleTo(ratType)
switch {
case typ.RType() == timeRType && st == Int && lt == Date:
case Istpy1Time && st == Int && lt == Date:
return &dateCodec{}

case typ.RType() == timeRType && st == Long && lt == TimestampMillis:
case Istpy1Time && st == Long && lt == TimestampMillis:
return &timestampMillisCodec{}

case typ.RType() == timeRType && st == Long && lt == TimestampMicros:
case Istpy1Time && st == Long && lt == TimestampMicros:
return &timestampMicrosCodec{}

case typ.RType() == ratRType && st == Bytes && lt == Decimal:
case Istpy1Rat && st == Bytes && lt == Decimal:
dec := ls.(*DecimalLogicalSchema)

return &bytesDecimalCodec{prec: dec.Precision(), scale: dec.Scale()}
Expand All @@ -126,12 +129,12 @@ func createDecoderOfNative(schema Schema, typ reflect2.Type) ValDecoder {
case reflect.Ptr:
ptrType := typ.(*reflect2.UnsafePtrType)
elemType := ptrType.Elem()

tpy1 := elemType.Type1()
ls := getLogicalSchema(schema)
if ls == nil {
break
}
if elemType.RType() != ratRType || schema.Type() != Bytes || ls.Type() != Decimal {
if !tpy1.ConvertibleTo(ratType) || schema.Type() != Bytes || ls.Type() != Decimal {
break
}
dec := ls.(*DecimalLogicalSchema)
Expand Down Expand Up @@ -242,17 +245,19 @@ func createEncoderOfNative(schema Schema, typ reflect2.Type) ValEncoder {
case reflect.Struct:
st := schema.Type()
lt := getLogicalType(schema)
tpy1 := typ.Type1()
Istpy1Time := tpy1.ConvertibleTo(timeType)
Istpy1Rat := tpy1.ConvertibleTo(ratType)
switch {
case typ.RType() == timeRType && st == Int && lt == Date:
case Istpy1Time && st == Int && lt == Date:
return &dateCodec{}

case typ.RType() == timeRType && st == Long && lt == TimestampMillis:
case Istpy1Time && st == Long && lt == TimestampMillis:
return &timestampMillisCodec{}

case typ.RType() == timeRType && st == Long && lt == TimestampMicros:
case Istpy1Time && st == Long && lt == TimestampMicros:
return &timestampMicrosCodec{}

case typ.RType() == ratRType && st != Bytes || lt == Decimal:
case Istpy1Rat && st != Bytes || lt == Decimal:
ls := getLogicalSchema(schema)
dec := ls.(*DecimalLogicalSchema)

Expand All @@ -265,12 +270,12 @@ func createEncoderOfNative(schema Schema, typ reflect2.Type) ValEncoder {
case reflect.Ptr:
ptrType := typ.(*reflect2.UnsafePtrType)
elemType := ptrType.Elem()

tpy1 := elemType.Type1()
ls := getLogicalSchema(schema)
if ls == nil {
break
}
if elemType.RType() != ratRType || schema.Type() != Bytes || ls.Type() != Decimal {
if !tpy1.ConvertibleTo(ratType) || schema.Type() != Bytes || ls.Type() != Decimal {
break
}
dec := ls.(*DecimalLogicalSchema)
Expand Down

0 comments on commit 9ec8016

Please sign in to comment.