From 2bde0bcf8b18c87cac0fa1262d6d2a5167c54218 Mon Sep 17 00:00:00 2001 From: Albert Skalt Date: Thu, 27 Jul 2023 12:21:42 +0300 Subject: [PATCH] tupleconv: implement basics - `Mapper[T]`: this in an interface for converting from a certain fixed type to any. Some basic mappers were implemented: parsers for tarantool types. - `Converter`: this is a struct, that converts tuples using `fmt`: mappers list. - `tt_helpers`: these are auxiliary functions used to build `fmt` for the `Converter`. Relates to #1 --- converter.go | 100 +++++++++++ converter_test.go | 179 +++++++++++++++++++ go.mod | 26 +++ go.sum | 53 ++++++ mapper.go | 265 ++++++++++++++++++++++++++++ mapper_test.go | 332 +++++++++++++++++++++++++++++++++++ tt_helpers.go | 117 +++++++++++++ tt_helpers_test.go | 425 +++++++++++++++++++++++++++++++++++++++++++++ tt_types.go | 39 +++++ 9 files changed, 1536 insertions(+) create mode 100644 converter.go create mode 100644 converter_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 mapper.go create mode 100644 mapper_test.go create mode 100644 tt_helpers.go create mode 100644 tt_helpers_test.go create mode 100644 tt_types.go diff --git a/converter.go b/converter.go new file mode 100644 index 0000000..184f26f --- /dev/null +++ b/converter.go @@ -0,0 +1,100 @@ +package tupleconv + +import ( + "errors" + "fmt" +) + +// ErrUnexpectedValue is unexpected value error. +type ErrUnexpectedValue struct { + val any +} + +// NewErrUnexpectedValue creates ErrUnexpectedValue by the value. +func NewErrUnexpectedValue(val any) error { + return &ErrUnexpectedValue{val: val} +} + +// Error is the error implementation for ErrUnexpectedValue. +func (err *ErrUnexpectedValue) Error() string { + return fmt.Sprintf("unexpected value: %v", err.val) +} + +var ErrWrongTupleLength = errors.New( + "tuple length should be equal to the length of the mappers list", +) + +var ErrInvalidFmt = errors.New("invalid fmt") + +// Converter performs tuple conversion. +type Converter[T any] struct { + fmt []Mapper[T] + isSingle bool +} + +// NewConverter creates Converter. +func NewConverter[T any](fmt []Mapper[T]) (*Converter[T], error) { + if len(fmt) == 0 { + return nil, ErrInvalidFmt + } + conv := &Converter[T]{fmt: fmt} + return conv, nil +} + +// NewConverterSingle creates a Converter with a single type as a format. +func NewConverterSingle[T any](fmt Mapper[T]) (*Converter[T], error) { + conv := &Converter[T]{ + fmt: []Mapper[T]{fmt}, + isSingle: true, + } + return conv, nil +} + +// validateTuple validates tuple in accordance with the Converter properties. +func (conv *Converter[T]) validateTuple(tuple []T) error { + if conv.isSingle { + return nil + } + if len(conv.fmt) != len(tuple) { + return ErrWrongTupleLength + } + return nil +} + +// ConvertVerbose converts tuple. +// It returns converted slice, conversion errors slice, API error. +func (conv *Converter[T]) ConvertVerbose(tuple []T) ([]any, []error, error) { + if err := conv.validateTuple(tuple); err != nil { + return nil, nil, err + } + result := make([]any, len(tuple)) + convErrors := make([]error, len(tuple)) + for i, field := range tuple { + c := conv.fmt[0] + if !conv.isSingle { + c = conv.fmt[i] + } + result[i], convErrors[i] = c.Map(field) + } + return result, convErrors, nil +} + +// Convert converts tuple until the first error. +func (conv *Converter[T]) Convert(tuple []T) ([]any, error) { + if err := conv.validateTuple(tuple); err != nil { + return nil, err + } + var err error + result := make([]any, len(tuple)) + for i, field := range tuple { + c := conv.fmt[0] + if !conv.isSingle { + c = conv.fmt[i] + } + result[i], err = c.Map(field) + if err != nil { + return nil, err + } + } + return result, nil +} diff --git a/converter_test.go b/converter_test.go new file mode 100644 index 0000000..76f128f --- /dev/null +++ b/converter_test.go @@ -0,0 +1,179 @@ +package tupleconv + +import ( + "errors" + "fmt" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +func TestValidateTuple(t *testing.T) { + sampleMapper := NewFuncMapper(func(t string) (any, error) { + return t + "1", nil + }) + + singleConv, err := NewConverterSingle[string](sampleMapper) + require.NoError(t, err) + + assert.NoError(t, singleConv.validateTuple([]string{"a", "b", "c"})) + assert.NoError(t, singleConv.validateTuple([]string{""})) + assert.NoError(t, singleConv.validateTuple([]string{"a"})) + + conv, err := NewConverter([]Mapper[string]{sampleMapper, sampleMapper}) + require.NoError(t, err) + + assert.NoError(t, conv.validateTuple([]string{"a", "b"})) + assert.Error(t, conv.validateTuple([]string{"a"})) + assert.Error(t, conv.validateTuple([]string{"a", "b", "c"})) +} + +func TestSingleConverter(t *testing.T) { + stringer := NewFuncMapper(func(t any) (any, error) { + return fmt.Sprintln(t), nil + }) + + encoder, err := NewConverterSingle[any](stringer) + require.NoError(t, err) + + cases := []struct { + name string + tuple []any + expected []any + }{ + { + name: "empty", + tuple: []any{}, + expected: []any{}, + }, + { + name: "different types", + tuple: []any{ + "a", + 1, + nil, + map[string]string{ + "1": "2", + "3": "4", + }, + }, + expected: []any{ + "a\n", + "1\n", + "\n", + "map[1:2 3:4]\n", + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + actual, err := encoder.Convert(tc.tuple) + assert.NoError(t, err) + assert.Equal(t, tc.expected, actual) + + actualVerbose, errorList, err := encoder.ConvertVerbose(tc.tuple) + assert.NoError(t, err) + assert.Equal(t, tc.expected, actualVerbose) + assert.Equal(t, len(tc.expected), len(errorList)) + for _, err := range errorList { + assert.NoError(t, err) + } + }) + } +} + +func TestConverter(t *testing.T) { + idealMapper := NewFuncMapper(func(t string) (any, error) { + return 42, nil + }) + + someError := errors.New("some error") + nonIdealMapper := NewFuncMapper(func(t string) (any, error) { + if t == "bad" { + return "", someError + } + return t, nil + }) + + decoder, err := NewConverter([]Mapper[string]{idealMapper, nonIdealMapper}) + require.NoError(t, err) + + t.Run("Convert", func(t *testing.T) { + cases := []struct { + name string + tuple []string + expectedTuple []any + wantErr bool + }{ + { + name: "all is ok", + tuple: []string{"1", "2"}, + expectedTuple: []any{42, "2"}, + }, + { + name: "wrong tuple length", + tuple: []string{"1"}, + wantErr: true, + }, + { + name: "decoding error", + tuple: []string{"1", "bad"}, + wantErr: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + actualTuple, err := decoder.Convert(tc.tuple) + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.expectedTuple, actualTuple) + } + }) + } + }) + + t.Run("ConvertVerbose", func(t *testing.T) { + cases := []struct { + name string + tuple []string + expectedTuple []any + expectedErrorList []error + wantAPIError bool + }{ + { + name: "all is ok", + tuple: []string{"1", "2"}, + expectedTuple: []any{42, "2"}, + expectedErrorList: []error{nil, nil}, + wantAPIError: false, + }, + { + name: "wrong tuple length", + tuple: []string{"1", "2", "3", "4"}, + wantAPIError: true, + }, + { + name: "decoding error", + tuple: []string{"1000", "bad"}, + expectedTuple: []any{42, ""}, + expectedErrorList: []error{nil, someError}, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + actualTuple, errorList, err := decoder.ConvertVerbose(tc.tuple) + if tc.wantAPIError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.expectedTuple, actualTuple) + assert.Equal(t, tc.expectedErrorList, errorList) + } + }) + } + }) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6b56b96 --- /dev/null +++ b/go.mod @@ -0,0 +1,26 @@ +module github.com/tarantool/go-tupleconv + +go 1.19 + +require ( + github.com/google/uuid v1.3.0 + github.com/shopspring/decimal v1.3.1 + github.com/stretchr/testify v1.7.1 + github.com/tarantool/go-tarantool v1.12.0 +) + +require ( + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/golang/protobuf v1.3.1 // indirect + github.com/mattn/go-pointer v0.0.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 // indirect + github.com/tarantool/go-openssl v0.0.8-0.20230307065445-720eeb389195 // indirect + github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + golang.org/x/net v0.0.0-20201021035429-f5854403a974 // indirect + golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 // indirect + google.golang.org/appengine v1.6.7 // indirect + gopkg.in/vmihailenco/msgpack.v2 v2.9.2 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..baf7dc8 --- /dev/null +++ b/go.sum @@ -0,0 +1,53 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +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/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0= +github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +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/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 h1:RC6RW7j+1+HkWaX/Yh71Ee5ZHaHYt7ZP4sQgUrm6cDU= +github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572/go.mod h1:w0SWMsp6j9O/dk4/ZpIhL+3CkG8ofA2vuv7k+ltqUMc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tarantool/go-openssl v0.0.8-0.20230307065445-720eeb389195 h1:/AN3eUPsTlvF6W+Ng/8ZjnSU6o7L0H4Wb9GMks6RkzU= +github.com/tarantool/go-openssl v0.0.8-0.20230307065445-720eeb389195/go.mod h1:M7H4xYSbzqpW/ZRBMyH0eyqQBsnhAMfsYk5mv0yid7A= +github.com/tarantool/go-tarantool v1.12.0 h1:JmTJDppt1hvSrI0iZKMocgWlBWMvEkhFGUZCgau9wS8= +github.com/tarantool/go-tarantool v1.12.0/go.mod h1:QRiXv0jnxwgxHtr9ZmifSr/eRba76gTUBgp69pDMX1U= +github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= +github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +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= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/vmihailenco/msgpack.v2 v2.9.2 h1:gjPqo9orRVlSAH/065qw3MsFCDpH7fa1KpiizXyllY4= +gopkg.in/vmihailenco/msgpack.v2 v2.9.2/go.mod h1:/3Dn1Npt9+MYyLpYYXjInO/5jvMLamn+AEGwNEOatn8= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/mapper.go b/mapper.go new file mode 100644 index 0000000..3bea86e --- /dev/null +++ b/mapper.go @@ -0,0 +1,265 @@ +package tupleconv + +import ( + "encoding/json" + "fmt" + "github.com/google/uuid" + "github.com/tarantool/go-tarantool/datetime" + "github.com/tarantool/go-tarantool/decimal" + "strconv" + "strings" + "time" +) + +// Mapper is a mapper from a certain type to any. +type Mapper[T any] interface { + Map(src T) (any, error) +} + +// Interface validations. +var ( + _ Mapper[string] = (*BoolParser)(nil) + _ Mapper[string] = (*UIntParser)(nil) + _ Mapper[string] = (*IntParser)(nil) + _ Mapper[string] = (*FloatParser)(nil) + _ Mapper[string] = (*DecimalParser)(nil) + _ Mapper[string] = (*UUIDParser)(nil) + _ Mapper[string] = (*StringParser)(nil) + _ Mapper[string] = (*DateTimeParser)(nil) + _ Mapper[string] = (*MapParser)(nil) + _ Mapper[string] = (*SliceParser)(nil) + _ Mapper[string] = (*BinaryParser)(nil) + _ Mapper[string] = (*NullParser)(nil) +) + +// FuncMapper is a function-based Mapper. +type FuncMapper[T any] struct { + mapFunc func(T) (any, error) +} + +// NewFuncMapper creates FuncMapper. +func NewFuncMapper[T any](mapFunc func(T) (any, error)) *FuncMapper[T] { + return &FuncMapper[T]{mapFunc: mapFunc} +} + +// Map is the implementation of Mapper for FuncMapper. +func (mp *FuncMapper[T]) Map(src T) (any, error) { + return mp.mapFunc(src) +} + +// MakeSequenceMapper makes a sequential Mapper from a Mappers list. +func MakeSequenceMapper[T any](mappers []Mapper[T]) Mapper[T] { + return NewFuncMapper(func(src T) (any, error) { + for _, mp := range mappers { + if result, err := mp.Map(src); err == nil { + return result, nil + } + } + return nil, NewErrUnexpectedValue(src) + }) +} + +// replaceSeparators replaces ignore characters and decimal separators. +func replaceSeparators(src, ignoreChars, decSeparators string) string { + for _, char := range ignoreChars { + src = strings.ReplaceAll(src, string(char), "") + } + for _, char := range decSeparators { + src = strings.ReplaceAll(src, string(char), ".") + } + return src +} + +// BoolParser is a parser to bool. +type BoolParser struct{} + +// NewBoolParser creates BoolParser. +func NewBoolParser() *BoolParser { + return &BoolParser{} +} + +// Map is the implementation of Mapper[string] for BoolParser. +func (*BoolParser) Map(src string) (any, error) { + return strconv.ParseBool(src) +} + +// UIntParser is a parser to uint64. +type UIntParser struct { + ignoreChars string +} + +// NewUIntParser creates UIntParser. +func NewUIntParser(ignoreChars string) *UIntParser { + return &UIntParser{ignoreChars: ignoreChars} +} + +// Map is the implementation of Mapper[string] for UIntParser. +func (parser *UIntParser) Map(src string) (any, error) { + src = replaceSeparators(src, parser.ignoreChars, "") + return strconv.ParseUint(src, 10, 64) +} + +// IntParser is a parser to int64. +type IntParser struct { + ignoreChars string +} + +// NewIntParser creates IntParser. +func NewIntParser(ignoreChars string) *IntParser { + return &IntParser{ignoreChars: ignoreChars} +} + +// Map is the implementation of Mapper[string] for IntParser. +func (parser *IntParser) Map(src string) (any, error) { + src = replaceSeparators(src, parser.ignoreChars, "") + return strconv.ParseInt(src, 10, 64) +} + +// FloatParser is a parser to float64. +type FloatParser struct { + ignoreChars string + decSeparators string +} + +// NewFloatParser creates FloatParser. +func NewFloatParser(ignoreChars, decSeparators string) *FloatParser { + return &FloatParser{ignoreChars: ignoreChars, decSeparators: decSeparators} +} + +// Map is the implementation of Mapper[string] for FloatParser. +func (parser *FloatParser) Map(src string) (any, error) { + src = replaceSeparators(src, parser.ignoreChars, parser.decSeparators) + return strconv.ParseFloat(src, 64) +} + +// DecimalParser is a parser to decimal.Decimal. +type DecimalParser struct { + ignoreChars string + decSeparators string +} + +// NewDecimalParser creates DecimalParser. +func NewDecimalParser(ignoreChars, decSeparators string) *DecimalParser { + return &DecimalParser{ignoreChars: ignoreChars, decSeparators: decSeparators} +} + +// Map is the implementation of Mapper[string] for DecimalParser. +func (parser *DecimalParser) Map(src string) (any, error) { + src = replaceSeparators(src, parser.ignoreChars, parser.decSeparators) + result, err := decimal.NewDecimalFromString(src) + if err == nil { + return *result, nil + } + return nil, err +} + +// UUIDParser is a parser to UUID. +type UUIDParser struct{} + +// NewUUIDParser creates UUIDParser. +func NewUUIDParser() *UUIDParser { + return &UUIDParser{} +} + +// Map is the implementation of Mapper[string] for UUIDParser. +func (*UUIDParser) Map(src string) (any, error) { + return uuid.Parse(src) +} + +// StringParser is a parser to string. +type StringParser struct{} + +// NewStringParser creates StringParser. +func NewStringParser() *StringParser { + return &StringParser{} +} + +// Map is the implementation of Mapper[string] for StringParser. +func (*StringParser) Map(src string) (any, error) { + return src, nil +} + +// DateTimeParser is a parser to datetime.Datetime. +type DateTimeParser struct{} + +// NewDateTimeParser creates DateTimeParser. +func NewDateTimeParser() *DateTimeParser { + return &DateTimeParser{} +} + +// Map is the implementation of Mapper[string] for DateTimeParser. +func (*DateTimeParser) Map(src string) (any, error) { + if result, err := time.Parse(time.RFC3339, src); err == nil { + return datetime.NewDatetime(result) + } + return nil, fmt.Errorf("unexpected date format") +} + +// MapParser is a parser to map. +// Only `json` is supported now. +type MapParser struct{} + +// NewMapParser creates MapParser. +func NewMapParser() *MapParser { + return &MapParser{} +} + +// Map is the implementation of Mapper[string] for MapParser. +func (*MapParser) Map(src string) (any, error) { + var result any + err := json.Unmarshal([]byte(src), &result) + if err != nil { + return nil, err + } + return result, nil +} + +// SliceParser is a parser to slice. +// Only `json` is supported now. +type SliceParser struct{} + +// NewSliceParser creates SliceParser. +func NewSliceParser() *SliceParser { + return &SliceParser{} +} + +// Map is the implementation of Mapper[string] for SliceParser. +func (*SliceParser) Map(src string) (any, error) { + var result any + err := json.Unmarshal([]byte(src), &result) + if err != nil { + return nil, err + } + return result, nil +} + +// BinaryParser is a parser to binary. +type BinaryParser struct{} + +// NewBinaryParser creates BinaryParser. +func NewBinaryParser() *BinaryParser { + return &BinaryParser{} +} + +// Map is the implementation of Mapper[string] for BinaryParser. +func (*BinaryParser) Map(src string) (any, error) { + return []byte(src), nil +} + +// NullParser is a parser to nil. +type NullParser struct { + nullValue string +} + +// NewNullParser creates NullParser. +func NewNullParser(nullValue string) *NullParser { + return &NullParser{nullValue: nullValue} +} + +// Map is the implementation of Mapper[string] for NullParser. +func (parser *NullParser) Map(src string) (any, error) { + if src == parser.nullValue { + return nil, nil + } + return nil, NewErrUnexpectedValue(src) +} diff --git a/mapper_test.go b/mapper_test.go new file mode 100644 index 0000000..800fce4 --- /dev/null +++ b/mapper_test.go @@ -0,0 +1,332 @@ +package tupleconv + +import ( + "github.com/google/uuid" + dec "github.com/shopspring/decimal" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tarantool/go-tarantool/datetime" + "github.com/tarantool/go-tarantool/decimal" + "math/big" + "testing" + "time" +) + +type parseCase struct { + value string + expected any + isErr bool +} + +func HelperTestMapper(t *testing.T, mp Mapper[string], cases []parseCase) { + for _, tc := range cases { + t.Run(tc.value, func(t *testing.T) { + result, err := mp.Map(tc.value) + if tc.isErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.expected, result) + } + }) + } +} + +func TestParsers(t *testing.T) { + someUUID, err := uuid.Parse("09b56913-11f0-4fa4-b5d0-901b5efa532a") + require.NoError(t, err) + nullUUID, err := uuid.Parse("00000000-0000-0000-0000-000000000000") + require.NoError(t, err) + + time1, err := time.Parse(time.RFC3339, "2020-08-22T11:27:43.123456789-02:00") + require.NoError(t, err) + dateTime1, err := datetime.NewDatetime(time1) + require.NoError(t, err) + + time2, err := time.Parse(time.RFC3339, "1880-01-01T00:00:00Z") + require.NoError(t, err) + dateTime2, err := datetime.NewDatetime(time2) + require.NoError(t, err) + + thSeparators := "@ '" + decSeparators := ".*" + + tests := map[Mapper[string]][]parseCase{ + NewBoolParser(): { + // Basic. + {value: "true", expected: true}, + {value: "false", expected: false}, + {value: "t", expected: true}, + {value: "f", expected: false}, + {value: "1", expected: true}, + {value: "0", expected: false}, + + // Error. + {value: "not bool at all", isErr: true}, + {value: "truth", isErr: true}, + }, + NewUIntParser(thSeparators): { + // Basic. + {value: "1", expected: uint64(1)}, + {value: "18446744073709551615", expected: uint64(18446744073709551615)}, + {value: "0", expected: uint64(0)}, + {value: "439423943289", expected: uint64(439423943289)}, + {value: "111'111'111'111", expected: uint64(111111111111)}, + + // Error. + {value: "18446744073709551616", isErr: true}, // Too big. + {value: "-101010", isErr: true}, + {value: "null", isErr: true}, + {value: "111`111", isErr: true}, + {value: "str", isErr: true}, + }, + NewIntParser(thSeparators): { + // Basic. + {value: "0", expected: int64(0)}, + {value: "1", expected: int64(1)}, + {value: "111'111'111'111", expected: int64(111111111111)}, + {value: "111@111@111'111", expected: int64(111111111111)}, + {value: "-1", expected: int64(-1)}, + {value: "9223372036854775807", expected: int64(9223372036854775807)}, + {value: "-9223372036854775808", expected: int64(-9223372036854775808)}, + {value: "-115 92 28 239", expected: int64(-1159228239)}, + + // Error. + {value: "-9223372036854775809", isErr: true}, // Too small. + {value: "9223372036854775808", isErr: true}, // Too big. + {value: "null", isErr: true}, + {value: "14,15", isErr: true}, + {value: "2.5", isErr: true}, + {value: "abacaba", isErr: true}, + }, + NewFloatParser(thSeparators, decSeparators): { + // Basic. + {value: "1.15", expected: 1.15}, + {value: "1e-2", expected: 0.01}, + {value: "-44", expected: float64(-44)}, + {value: "1.447e+44", expected: 1.447e+44}, + {value: "1", expected: float64(1)}, + {value: "-1", expected: float64(-1)}, + {value: "18446744073709551615", expected: float64(18446744073709551615)}, + {value: "18446744073709551616", expected: float64(18446744073709551615)}, + {value: "0", expected: float64(0)}, + {value: "-9223372036854775808", expected: float64(-9223372036854775808)}, + {value: "-9223372036854775809", expected: float64(-9223372036854775808)}, + {value: "439423943289", expected: float64(439423943289)}, + {value: "1.15", expected: 1.15}, + {value: "1e-2", expected: 0.01}, + {value: "1.447e+44", expected: 1.447e+44}, + {value: "1 2 3 @ 4", expected: float64(1234)}, + {value: "1 2 3 * 4", expected: 123.4}, + + // Error. + {value: "1'2'3'4**5", isErr: true}, + {value: "notnumberatall", isErr: true}, + {value: `{"a":3}`, isErr: true}, + }, + NewDateTimeParser(): { + // Basic. + {value: "2020-08-22T11:27:43.123456789-02:00", expected: dateTime1}, + {value: "1880-01-01T00:00:00Z", expected: dateTime2}, + + // Error. + {value: "19-19-19", isErr: true}, + {value: "#$,%13п", isErr: true}, + {value: "2020-08-22T11:27:43*123456789-02:00", isErr: true}, + }, + NewUUIDParser(): { + // Basic. + {value: "09b56913-11f0-4fa4-b5d0-901b5efa532a", expected: someUUID}, + {value: "00000000-0000-0000-0000-000000000000", expected: nullUUID}, + + // Error. + {value: "09b56913-11f0-4fa4-b5d0-901b5efa532", isErr: true}, + }, + NewSliceParser(): { + // Basic. + { + value: "[1, 2, 3, 4]", + expected: []any{float64(1), float64(2), float64(3), float64(4)}}, + { + value: `[1, {"a" : [2,3]}]`, + expected: []any{ + float64(1), + map[string]any{ + "a": []any{float64(2), float64(3)}, + }, + }, + }, + { + value: "[null, null, null]", + expected: []any{nil, nil, nil}, + }, + + // Error. + {value: "[1,2,3,", isErr: true}, + {value: "[pqp][qpq]", isErr: true}, + }, + NewMapParser(): { + // Basic. + { + value: `{"a":2, "b":3, "c":{ "d":"str" }}`, + expected: map[string]any{ + "a": float64(2), + "b": float64(3), + "c": map[string]any{ + "d": "str", + }, + }, + }, + { + value: `{"1": [1,2,3], "2": {"a":4} }`, + expected: map[string]any{ + "1": []any{float64(1), float64(2), float64(3)}, + "2": map[string]any{ + "a": float64(4), + }, + }, + }, + { + value: `{"1" : null, "2" : null}`, + expected: map[string]any{ + "1": nil, + "2": nil, + }, + }, + + // Error. + {value: `{1:"2"}`, isErr: true}, + {value: `{"a":2`, isErr: true}, + {value: `str`, isErr: true}, + }, + NewBinaryParser(): { + // Basic. + {value: "\x01\x02\x03", expected: []byte{1, 2, 3}}, + {value: "abc", expected: []byte("abc")}, + }, + NewStringParser(): { + // Basic. + {value: "blablabla", expected: "blablabla"}, + {value: "бк#132433#$,%13п", expected: "бк#132433#$,%13п"}, + {value: "null", expected: "null"}, + }, + NewNullParser("null"): { + // Basic. + {value: "null", expected: nil}, + + // Error. + {value: "505", isErr: true}, + {value: "nil", isErr: true}, + }, + } + + for parser, cases := range tests { + HelperTestMapper(t, parser, cases) + } +} + +func TestParseDecimal(t *testing.T) { + parser := NewDecimalParser("# ", ".*") + + cases := []struct { + value string + expected any + isErr bool + }{ + // Basic. + {value: "0", expected: dec.NewFromBigInt(big.NewInt(0), 0)}, + {value: "1", expected: dec.NewFromBigInt(big.NewInt(1), 0)}, + {value: "-1", expected: dec.NewFromBigInt(big.NewInt(-1), 0)}, + {value: "43904329", expected: dec.NewFromBigInt(big.NewInt(43904329), 0)}, + {value: "-9223372036854775808", expected: dec.NewFromBigInt(big.NewInt(int64( + -9223372036854775808)), + 0)}, + {value: "1.447e+44", expected: dec.NewFromBigInt(big.NewInt(1447), 41)}, + {value: "1*5", expected: dec.NewFromBigInt(big.NewInt(15), -1)}, + + // Error. + {value: "abacaba", isErr: true}, + {value: "-1/0", isErr: true}, + {value: "1**5", isErr: true}, + } + for _, tc := range cases { + t.Run(tc.value, func(t *testing.T) { + result, err := parser.Map(tc.value) + if !tc.isErr { + assert.NoError(t, err) + expAsDecimal, isExpDecimal := tc.expected.(dec.Decimal) + if !isExpDecimal { + assert.Equal(t, nil, result) + } else { + asDecimal, ok := result.(decimal.Decimal) + assert.True(t, ok) + assert.True(t, asDecimal.Equal(expAsDecimal)) + } + } else { + assert.Error(t, err) + } + }) + } +} + +func TestMakeSequenceMapper(t *testing.T) { + parser := MakeSequenceMapper([]Mapper[string]{ + &UIntParser{}, + &IntParser{}, + &FloatParser{}, + &MapParser{}, + }) + + cases := []parseCase{ + // Basic. + {value: "0", expected: uint64(0)}, + {value: "1", expected: uint64(1)}, + {value: "-10", expected: int64(-10)}, + {value: "2.5", expected: 2.5}, + {value: "{}", expected: map[string]any{}}, + {value: "null", expected: nil}, // As `json`. + + // Error. + {value: "12-13-14", isErr: true}, + {value: "12,14", isErr: true}, + } + HelperTestMapper(t, parser, cases) +} + +func TestReplaceSeparators(t *testing.T) { + cases := []struct { + ignoreChars string + decSeparators string + value string + expected string + }{ + { + ignoreChars: " '`", + decSeparators: "", + value: "", + expected: "", + }, + { + ignoreChars: " '`", + decSeparators: "", + value: "123 '456` 789", + expected: "123456789", + }, + { + ignoreChars: "", + decSeparators: "@*", + value: "abc@*abc", + expected: "abc..abc", + }, + { + ignoreChars: "-, ", + decSeparators: "e", + value: "-12, 45, 36 e 96", + expected: "124536.96", + }, + } + + for _, tc := range cases { + assert.Equal(t, tc.expected, replaceSeparators(tc.value, tc.ignoreChars, tc.decSeparators)) + } +} diff --git a/tt_helpers.go b/tt_helpers.go new file mode 100644 index 0000000..e21d038 --- /dev/null +++ b/tt_helpers.go @@ -0,0 +1,117 @@ +package tupleconv + +import "errors" + +// ErrInvalidSpaceFmt is an error of the space format. +var ErrInvalidSpaceFmt = errors.New("invalid space format") + +const ( + kType = "type" + kIsNullable = "is_nullable" +) + +const ( + defaultIgnoreChars = "" + defaultDecSeparators = "." + defaultNullValue = "" +) + +// MappersTable can return a Mapper[string] based on the type name. +type MappersTable interface { + getMapper(typ TypeName) (Mapper[string], bool) +} + +// ParsersTTTable is basic tarantool parsers table. +type ParsersTTTable struct{} + +// getMapper is the implementation of MappersTable for MappersTTTable. +func (ptable *ParsersTTTable) getMapper(typ TypeName) (Mapper[string], bool) { + mapper, ok := parserByTTType[typ] + return mapper, ok +} + +// parserByTTType is a mapping from tarantool typenames to parsers. +var parserByTTType = map[TypeName]Mapper[string]{ + TypeBoolean: NewBoolParser(), + TypeString: NewStringParser(), + TypeUnsigned: NewUIntParser(defaultIgnoreChars), + TypeDatetime: NewDateTimeParser(), + TypeUUID: NewUUIDParser(), + TypeMap: NewMapParser(), + TypeArray: NewSliceParser(), + TypeVarbinary: NewBinaryParser(), + + TypeDouble: NewFloatParser(defaultIgnoreChars, defaultDecSeparators), + TypeDecimal: NewDecimalParser(defaultIgnoreChars, defaultDecSeparators), + + TypeInteger: MakeSequenceMapper([]Mapper[string]{ + NewUIntParser(defaultIgnoreChars), + NewIntParser(defaultIgnoreChars), + }), + + TypeNumber: MakeSequenceMapper([]Mapper[string]{ + NewUIntParser(defaultIgnoreChars), + NewIntParser(defaultIgnoreChars), + NewFloatParser(defaultIgnoreChars, defaultDecSeparators), + }), + + TypeAny: MakeSequenceMapper([]Mapper[string]{ + NewUIntParser(defaultIgnoreChars), + NewIntParser(defaultIgnoreChars), + NewFloatParser(defaultIgnoreChars, defaultDecSeparators), + NewDecimalParser(defaultIgnoreChars, defaultDecSeparators), + NewBoolParser(), + NewDateTimeParser(), + NewUUIDParser(), + NewStringParser(), // Always success. + }), + + // Same as any by default. + TypeScalar: MakeSequenceMapper([]Mapper[string]{ + NewUIntParser(defaultIgnoreChars), + NewIntParser(defaultIgnoreChars), + NewFloatParser(defaultIgnoreChars, defaultDecSeparators), + NewDecimalParser(defaultIgnoreChars, defaultDecSeparators), + NewBoolParser(), + NewDateTimeParser(), + NewUUIDParser(), + NewStringParser(), // Always success. + }), +} + +var ttParsers MappersTable = &ParsersTTTable{} + +// NewParserFor returns parser by the type name and `isNullable` property. +var NewParserFor = func(typ TypeName, isNullable bool) (Mapper[string], error) { + parser, isSupported := ttParsers.getMapper(typ) + if !isSupported { + return nil, NewErrUnexpectedType(typ) + } + if isNullable { + parser = MakeSequenceMapper([]Mapper[string]{ + parser, + NewNullParser(defaultNullValue), + }) + } + return parser, nil +} + +// GetStringsToTTFmt creates Mapper list by the space format. +func GetStringsToTTFmt(spaceFmt []map[string]any) ([]Mapper[string], error) { + var err error + fmt := make([]Mapper[string], len(spaceFmt)) + for i, fieldFmt := range spaceFmt { + typStr, ok := fieldFmt[kType].(string) + if !ok { + return nil, ErrInvalidSpaceFmt + } + typ := TypeName(typStr) + isNullable, ok := fieldFmt[kIsNullable].(bool) + isNullable = isNullable && ok + fmt[i], err = NewParserFor(typ, isNullable) + if err != nil { + return nil, err + } + } + return fmt, nil +} diff --git a/tt_helpers_test.go b/tt_helpers_test.go new file mode 100644 index 0000000..c6e4a5a --- /dev/null +++ b/tt_helpers_test.go @@ -0,0 +1,425 @@ +package tupleconv + +import ( + "github.com/google/uuid" + dec "github.com/shopspring/decimal" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tarantool/go-tarantool/datetime" + "github.com/tarantool/go-tarantool/decimal" + "math/big" + "testing" + "time" +) + +type mockTable struct { + appeals []TypeName +} + +func (m *mockTable) getMapper(typ TypeName) (Mapper[string], bool) { + m.appeals = append(m.appeals, typ) + if typ == "fake" { + return nil, false + } + return &IntParser{}, true +} + +func TestImplNewParserFor(t *testing.T) { + oldTTParsers := ttParsers + defer func() { + ttParsers = oldTTParsers + }() + m := &mockTable{appeals: []TypeName{}} + ttParsers = m + + _, err := NewParserFor("fake", false) + assert.Error(t, err) + + _, err = NewParserFor("bool", true) + assert.NoError(t, err) + + _, err = NewParserFor("int", false) + assert.NoError(t, err) + assert.Equal(t, []TypeName{"fake", "bool", "int"}, m.appeals) +} + +func TestNewParserFor(t *testing.T) { + someUUID, err := uuid.Parse("09b56913-11f0-4fa4-b5d0-901b5efa532a") + require.NoError(t, err) + nullUUID, err := uuid.Parse("00000000-0000-0000-0000-000000000000") + require.NoError(t, err) + + time1, err := time.Parse(time.RFC3339, "2020-08-22T11:27:43.123456789-02:00") + require.NoError(t, err) + dateTime1, err := datetime.NewDatetime(time1) + require.NoError(t, err) + + time2, err := time.Parse(time.RFC3339, "1880-01-01T00:00:00Z") + require.NoError(t, err) + dateTime2, err := datetime.NewDatetime(time2) + require.NoError(t, err) + + tests := map[TypeName][]struct { + value string + expected any + isNullable bool + isErr bool + }{ + TypeBoolean: { + // Basic. + {value: "true", expected: true}, + {value: "false", expected: false}, + {value: "t", expected: true}, + {value: "f", expected: false}, + {value: "1", expected: true}, + {value: "0", expected: false}, + + // Nullable. + {value: "", isNullable: true, expected: nil}, + + // Error. + {value: "not bool at all", isErr: true}, + {value: "truth", isErr: true}, + }, + TypeInteger: { + // Basic. + {value: "0", expected: uint64(0)}, + {value: "1", expected: uint64(1)}, + {value: "-1", expected: int64(-1)}, + {value: "18446744073709551615", expected: uint64(18446744073709551615)}, + {value: "-9223372036854775808", expected: int64(-9223372036854775808)}, + + // Nullable. + {value: "", isNullable: true, expected: nil}, + + // Error. + {value: "-4329423948329482394328492349238", isErr: true}, // Too small. + {value: "12`13`144", isNullable: true, isErr: true}, + {value: "nil", isNullable: true, isErr: true}, + {value: "null", isErr: true}, + {value: "14,15", isErr: true}, + {value: "2.5", isErr: true}, + {value: "abacaba", isErr: true}, + }, + TypeUnsigned: { + // Basic. + {value: "1", expected: uint64(1)}, + {value: "18446744073709551615", expected: uint64(18446744073709551615)}, + {value: "0", expected: uint64(0)}, + {value: "439423943289", expected: uint64(439423943289)}, + + // Nullable. + {value: "", isNullable: true, expected: nil}, + + // Error. + {value: "18446744073709551616", isErr: true}, // Too big. + {value: "null", isErr: true}, + {value: "111`111", isErr: true}, + {value: "str", isErr: true}, + }, + TypeNumber: { + // Basic. + {value: "1.15", expected: 1.15}, + {value: "1e-2", expected: 0.01}, + {value: "-44", expected: int64(-44)}, + {value: "1.447e+44", expected: 1.447e+44}, + {value: "1", expected: uint64(1)}, + {value: "-1", expected: int64(-1)}, + {value: "18446744073709551615", expected: uint64(18446744073709551615)}, + {value: "18446744073709551616", expected: float64(18446744073709551615)}, + {value: "0", expected: uint64(0)}, + {value: "-9223372036854775808", expected: int64(-9223372036854775808)}, + {value: "-9223372036854775809", expected: float64(-9223372036854775808)}, + {value: "439423943289", expected: uint64(439423943289)}, + {value: "1.15", expected: 1.15}, + {value: "1e-2", expected: 0.01}, + {value: "1.447e+44", expected: 1.447e+44}, + + // Nullable. + {value: "", isNullable: true, expected: nil}, + + // Error. + {value: "1'2'3'4**5", isErr: true}, + {value: "notnumberatall", isErr: true}, + {value: `{"a":3}`, isErr: true}, + }, + TypeDatetime: { + // Basic. + {value: "2020-08-22T11:27:43.123456789-02:00", expected: dateTime1}, + {value: "1880-01-01T00:00:00Z", expected: dateTime2}, + + // Nullable. + {value: "", isNullable: true, expected: nil}, + + // Error. + {value: "19-19-19", isErr: true}, + {value: "#$,%13п", isErr: true}, + {value: "2020-08-22T11:27:43*123456789-02:00", isErr: true}, + }, + TypeUUID: { + // Basic. + {value: "09b56913-11f0-4fa4-b5d0-901b5efa532a", expected: someUUID}, + {value: "00000000-0000-0000-0000-000000000000", expected: nullUUID}, + + // Nullable. + {value: "", isNullable: true, expected: nil}, + + // Error. + {value: "09b56913-11f0-4fa4-b5d0-901b5efa532", isErr: true}, + }, + TypeArray: { + // Basic. + { + value: "[1, 2, 3, 4]", + expected: []any{float64(1), float64(2), float64(3), float64(4)}}, + { + value: `[1, {"a" : [2,3]}]`, + expected: []any{ + float64(1), + map[string]any{ + "a": []any{float64(2), float64(3)}, + }, + }, + }, + {value: "null", expected: nil}, + + // Nullable. + {value: "", isNullable: true, expected: nil}, + + // Error. + {value: "[1,2,3,", isErr: true}, + {value: "[pqp][qpq]", isErr: true}, + }, + TypeMap: { + // Basic. + { + value: `{"a":2, "b":3, "c":{ "d":"str" }}`, + expected: map[string]any{ + "a": float64(2), + "b": float64(3), + "c": map[string]any{ + "d": "str", + }, + }, + }, + { + value: `{"1": [1,2,3], "2": {"a":4} }`, + expected: map[string]any{ + "1": []any{float64(1), float64(2), float64(3)}, + "2": map[string]any{ + "a": float64(4), + }, + }, + }, + {value: "null", expected: nil}, + + // Nullable. + {value: "", isNullable: true, expected: nil}, + + // Error. + {value: `{1:"2"}`, isErr: true}, + {value: `{"a":2`, isErr: true}, + {value: `str`, isErr: true}, + {value: "{null}", isNullable: true, isErr: true}, + }, + TypeVarbinary: { + // Basic. + { + value: "\x01\x02\x03", + expected: []byte{1, 2, 3}, + }, + { + value: "abc", + expected: []byte("abc"), + }, + + // Nullable. + // Parsing to the primary type is prioritized over null parsing. + {value: "", isNullable: true, expected: []byte{}}, + }, + TypeString: { + {value: "blablabla", expected: "blablabla"}, + {value: "бк#132433#$,%13п", expected: "бк#132433#$,%13п"}, + {value: "null", expected: "null"}, + + // Nullable. + // Parsing to the primary type is prioritized over null parsing. + {value: "", isNullable: true, expected: ""}, + }, + TypeAny: { + {value: "blablabla", expected: "blablabla"}, + {value: "0", expected: uint64(0)}, + {value: "1", expected: uint64(1)}, + {value: "-9223372036854775808", expected: int64(-9223372036854775808)}, + {value: "-9223372036854775809", expected: float64(-9223372036854775808)}, + {value: "true", expected: true}, + {value: "false", expected: false}, + {value: "", isNullable: true, expected: ""}, + {value: "09b56913-11f0-4fa4-b5d0-901b5efa532a", expected: someUUID}, + {value: "2020-08-22T11:27:43.123456789-02:00", expected: dateTime1}, + }, + + TypeScalar: { + {value: "blablabla", expected: "blablabla"}, + {value: "0", expected: uint64(0)}, + {value: "1", expected: uint64(1)}, + {value: "-9223372036854775808", expected: int64(-9223372036854775808)}, + {value: "-9223372036854775809", expected: float64(-9223372036854775808)}, + {value: "true", expected: true}, + {value: "false", expected: false}, + {value: "", isNullable: true, expected: ""}, + {value: "09b56913-11f0-4fa4-b5d0-901b5efa532a", expected: someUUID}, + {value: "2020-08-22T11:27:43.123456789-02:00", expected: dateTime1}, + }, + } + + for typ, cases := range tests { + for _, tc := range cases { + t.Run(string(typ)+" "+tc.value, func(t *testing.T) { + parser, err := NewParserFor(typ, tc.isNullable) + assert.NoError(t, err) + actual, err := parser.Map(tc.value) + if tc.isErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.expected, actual) + } + }) + } + } + + t.Run("API error", func(t *testing.T) { + _, err := NewParserFor("fake", true) + assert.Error(t, err) + }) +} + +func TestNewParserForDecimal(t *testing.T) { + cases := []struct { + value string + expected any + isNullable bool + isErr bool + }{ + // Basic. + {value: "0", expected: dec.NewFromBigInt(big.NewInt(0), 0)}, + {value: "1", expected: dec.NewFromBigInt(big.NewInt(1), 0)}, + {value: "-1", expected: dec.NewFromBigInt(big.NewInt(-1), 0)}, + {value: "43904329", expected: dec.NewFromBigInt(big.NewInt(43904329), 0)}, + {value: "-9223372036854775808", expected: dec.NewFromBigInt(big.NewInt(int64( + -9223372036854775808)), + 0)}, + {value: "1.447e+44", expected: dec.NewFromBigInt(big.NewInt(1447), 41)}, + + // Nullable. + {value: "", isNullable: true, expected: nil}, + + // Error. + {value: "abacaba", isErr: true}, + {value: "-1/0", isErr: true}, + {value: "1**5", isErr: true}, + } + for _, tc := range cases { + t.Run(tc.value, func(t *testing.T) { + parser, _ := NewParserFor(TypeDecimal, tc.isNullable) + result, err := parser.Map(tc.value) + if !tc.isErr { + assert.NoError(t, err) + expAsDecimal, isExpDecimal := tc.expected.(dec.Decimal) + if !isExpDecimal { + assert.Equal(t, nil, result) + } else { + asDecimal, ok := result.(decimal.Decimal) + assert.True(t, ok) + assert.True(t, asDecimal.Equal(expAsDecimal)) + } + } else { + assert.Error(t, err) + } + }) + } +} + +func TestImplGetStringsToTTFmt(t *testing.T) { + oldNewParserFor := NewParserFor + defer func() { + NewParserFor = oldNewParserFor + }() + + type appeal struct { + typ TypeName + isNullable bool + } + appeals := make([]appeal, 0) + + NewParserFor = func(typ TypeName, isNullable bool) (Mapper[string], error) { + appeals = append(appeals, appeal{typ: typ, isNullable: isNullable}) + if typ == "fake" { + return nil, NewErrUnexpectedType(typ) + } + return &StringParser{}, nil + } + + cases := []struct { + name string + spaceFmt []map[string]any + isErr bool + expectedAppeals []appeal + }{ + { + name: "basic", + spaceFmt: []map[string]any{ + {"type": "int"}, + {"type": "bool", "is_nullable": true}, + {"type": "string", "is_nullable": false}, + {"type": "string", "???": 1.5}, + }, + expectedAppeals: []appeal{ + {"int", false}, + {"bool", true}, + {"string", false}, + {"string", false}, + }, + }, + { + name: "no type", + spaceFmt: []map[string]any{ + {"type": "int"}, + {"type": "bool", "is_nullable": true}, + {"is_nullable": true}, + }, + isErr: true, + expectedAppeals: []appeal{ + {"int", false}, + {"bool", true}, + {"", true}, + }, + }, + { + name: "unexpected type", + spaceFmt: []map[string]any{ + {"type": "int"}, + {"type": "fake", "is_nullable": true}, + }, + isErr: true, + expectedAppeals: []appeal{ + {"int", false}, + {"fake", true}, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + appeals = appeals[:0] + _, err := GetStringsToTTFmt(tc.spaceFmt) + if tc.isErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.expectedAppeals, appeals) + } + }) + } +} diff --git a/tt_types.go b/tt_types.go new file mode 100644 index 0000000..3e4e8f3 --- /dev/null +++ b/tt_types.go @@ -0,0 +1,39 @@ +package tupleconv + +import "fmt" + +// TypeName is the data type for type names. +type TypeName string + +// ErrUnexpectedType is unexpected type error. +type ErrUnexpectedType struct { + typ TypeName +} + +// NewErrUnexpectedType creates ErrUnexpectedType by the type name. +func NewErrUnexpectedType(typ TypeName) error { + return &ErrUnexpectedType{typ: typ} +} + +// Error is the error implementation for ErrUnexpectedType. +func (err *ErrUnexpectedType) Error() string { + return fmt.Sprintf("unexpected type %v", err.typ) +} + +// Types are supported tarantool types. +const ( + TypeBoolean TypeName = "boolean" + TypeString TypeName = "string" + TypeInteger TypeName = "integer" + TypeUnsigned TypeName = "unsigned" + TypeDouble TypeName = "double" + TypeNumber TypeName = "number" + TypeDecimal TypeName = "decimal" + TypeDatetime TypeName = "datetime" + TypeUUID TypeName = "uuid" + TypeArray TypeName = "array" + TypeMap TypeName = "map" + TypeVarbinary TypeName = "varbinary" + TypeScalar TypeName = "scalar" + TypeAny TypeName = "any" +)